Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 65 additions & 15 deletions .agents/skills/jaws/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ metadata:

# JaWS application design

Build the smallest direct projection of synchronized server state. JaWS is an
immediate-mode, server-driven UI framework, not MVC.
Build the UI directly from synchronized server state. JaWS is an immediate-mode,
server-driven UI framework, not MVC. Authoritative server state is the only
application state model; this is a design requirement, not a preference.

## Match the application version first

Expand Down Expand Up @@ -46,20 +47,35 @@ does not contain a guide, inspect its exported docs and source instead.
- Do not retain JaWS Requests, Elements, or UI definitions in application or
domain state. JaWS owns that live tree.

Default to definition dots that retain stable pointers to authoritative
synchronized state. Render and update through direct binders and synchronized
getters over that state. Do not add page-, screen-, state-, or component-shaped
render DTOs or broad snapshots merely to make template execution atomic. JaWS
requires race-free reads and correct dependency invalidation, not a transaction
across an entire fragment. Do not hold an application lock across template
execution to manufacture one. Separate reads may observe adjacent valid states;
after mutation, dirty the relevant dependencies so the UI converges.
Definition dots should retain stable pointers to authoritative synchronized
state. Render and update through direct binders and synchronized getters over
that state.

Application code MUST NOT create a second representation of application state
for rendering. Page, screen, state, component, view-model, or DTO structs that
copy authoritative fields for templates, getters, attributes, or updates are
forbidden. Renaming such a copy does not make it acceptable. If a proposed type
exists only to carry render data copied from domain state, stop and redesign the
UI to read the authoritative state directly.

Application code MUST NOT use broad snapshots to manufacture fragment-wide
atomic rendering. JaWS requires race-free reads and correct dependency
invalidation, not a transaction across an entire fragment. Do not hold an
application lock across template execution to manufacture one. Separate reads
may observe adjacent valid states; dirty dispatch is the consistency mechanism
that makes matching Elements converge on current authoritative state.

When mutation code owns the writes, it MUST return or dirty the exact dependency
tags whose rendered output may change. It MUST NOT snapshot application state
only to diff it afterward and discover which ordinary mutation tags to dirty.
Derive that tag set directly from the mutation semantics.

Copying mutable collection storage under its lock so a caller can iterate after
unlock is a synchronization boundary, not a presentation snapshot. Capture
multiple primitives together only when one widget, attribute set, or operation
requires an invariant; keep that capture local instead of expanding it into a
fragment-wide render model.
requires an invariant. Such a capture MUST remain local to that operation; it
must not become a retained render object, template Dot, or fragment-wide state
model.

For a Container, a freshly returned equal child is a reconciliation key. JaWS
reuses the existing Element and its original UI value; it does not replace
Expand All @@ -73,6 +89,25 @@ equality or be read indirectly from synchronized mutable state.

## Choose the smallest rendering primitive

- When an authoritative source implements the getter and event interfaces for a
standard widget, pass that source directly as the widget's primary argument,
for example `{{$.Button .Action}}`. When the read is naturally a functor,
adapt it with `bind.HTMLGetterFunc`; for text, use `bind.StringGetterFunc` and
let the widget's `bind.MakeHTMLGetter` conversion escape it. A bound value can
customize markup with `Binder.GetHTML` while remaining a standard
`HTMLGetter`.
- An application UI may overload a standard widget's render or update behavior,
but this is discouraged when a standard getter, binder, semantic `ui.Object`,
or render parameter expresses the same control. Keep the overload only when it
adds behavior the standard composition cannot provide. Embed or retain the
standard widget, document the exact phase behavior being replaced, and keep
the outer definition as the Element's single UI value for both phases.
- By default, HTML-inner widgets call their getters during initial rendering and
dirty updates; a justified outer updater may replace the dirty phase. If a
getter queues wrapper attributes, `TailHTML` may repeat attributes already
emitted inline. Treat an overload that suppresses that payload as a performance
change: retain a benchmark and weigh the measured result against the simpler
standard composition.
- **Full HTML document:** use `ui.Handler`. It creates the JaWS Request, applies
`no-store`, renders without a generated wrapper, and treats the page Dot as
arbitrary template data rather than a tag or equality key. The page
Expand Down Expand Up @@ -181,11 +216,19 @@ helpers.
and attrs from Dot for its generated wrapper.
- Render params contribute literal attributes and register recognized handlers
and tags. A parameter-valued `InitialHTMLAttrHandler` is not invoked.
- Prefer an ordinary render parameter for attributes specific to one widget use.
Put `InitialHTMLAttrHandler` on a shared getter only when every widget using
that getter should inherit those attributes.
- `ui.NewTemplate(tag, name, dot, attrs...)` accepts trusted raw wrapper attributes,
which participate in Template equality. For duplicate names, precedence is render
params, constructor attrs, then Dot attrs.
- Initial attrs run once for that Element. Dirty updates do not rerun them.
Change dynamic attrs through Element update methods or replace the Element.
- A `template.HTMLAttr` result is raw opening-tag syntax, not a DOM update or
attribute diff. By itself it identifies neither removals nor attribute
ownership. Never reinterpret an initial-attribute hook as an update getter; a
reusable dynamic-attribute contract must define ownership and diff semantics,
preferably through explicit structured set/remove operations.
- A retained Template wrapper keeps its attributes while its recreated
descendants run their own initial attrs.
- Object attribute hooks concatenate. Binder attribute hooks run with the Binder
Expand All @@ -202,6 +245,11 @@ untrusted values with `htmlio.Attr` and a trusted name; convert the result to

## Render shape and verification

- Before implementing, identify the authoritative state, its synchronization,
the direct binders/getters that read it, and the precise dependencies whose
rendered output each mutation can change. If the plan includes a
render-specific copy of domain fields or a snapshot/diff layer for ordinary
dirty tracking, stop and redesign it first.
- Keep HTML structure in templates and state mutations out of getter/render
paths. When one direct getter result must remain consistent within a template
execution, assign that value to a local template variable. This is not a
Expand All @@ -216,7 +264,9 @@ untrusted values with `htmlio.Attr` and a trusted name; convert the result to
connect, and disconnect behavior.
- Test pure domain transitions separately from JaWS transport.

Before finishing, confirm the design does not introduce an application-owned UI
tree, a full-document Template, screen-shaped render state, pointer-wrapped
The following are completion blockers. Do not finish while the design contains
an application-owned UI tree, a full-document Template, any render DTO or
retained presentation snapshot, screen-shaped render state, pointer-wrapped
definition values, mutable tag identity, duplicate JaWS IDs, direct locked-field
assignment, or broad dirtying that masks dependency errors.
assignment, snapshot/diff dirty tracking for mutation-owned writes, or broad
dirtying that masks dependency errors. Refactor the violation before continuing.
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,11 @@ func main() {
}
```

Next steps usually include adding templates with `AddTemplateLookuper`, creating
types that implement `JawsRender` and `JawsUpdate`, and introducing sessions for
per-user state.
Next steps usually include composing standard widgets and binders, creating
semantic controls with `ui.Object`, and registering precise dependency tags.
The [Minesweeper example](./examples/minesweeper/) shows those patterns in a
complete collaborative application. Introduce sessions when state should belong
to an individual user.

## Production guidance

Expand Down Expand Up @@ -150,5 +152,7 @@ JaWS keeps dependencies outside the standard library to a minimum:
the complete package-guide index.
* Inspect the compile-checked [examples](./examples/example_test.go) to copy and
adapt the setup sequence.
* Run the [Minesweeper example](./examples/minesweeper/) to explore targeted
updates in a complete server-driven UI.
* Explore the [demo application](https://github.com/linkdata/jawsdemo) for a more
complete project structure.
18 changes: 14 additions & 4 deletions contracts.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,11 @@ type Updater interface {
// JawsUpdate is called for an [Element] that has been marked dirty to update its HTML.
// Do not call this yourself unless it is from within another JawsUpdate implementation.
// The engine does not invoke this once the [Element] is deleted (see [Element.Deleted]).
// A UI implementation that delegates rendering and updating must delegate both calls
// to the same UI widget. Rendering elem through one widget and updating it through
// another is unsupported.
//
// A UI may embed or retain a standard widget and override one phase while the outer
// value remains elem's UI. It is responsible for preserving that widget's lifecycle,
// ownership, and multiplicity contracts. Delegating render and update calls to two
// unrelated widgets is unsupported.
JawsUpdate(elem *Element)
}

Expand Down Expand Up @@ -156,7 +158,15 @@ type ContextMenuHandler interface {

// InitialHTMLAttrHandler provides attributes for initial [Element] rendering.
type InitialHTMLAttrHandler interface {
// JawsInitialHTMLAttr returns attributes for elem's initial render, or an empty string.
// JawsInitialHTMLAttr returns trusted attributes for elem's opening tag.
//
// Standard renderers invoke it during initial rendering only; dirty updates do
// not invoke it. The result is raw trusted [html/template.HTMLAttr] syntax and
// must not contain unescaped untrusted data. JaWS neither parses it nor retains
// attribute ownership or a baseline for later updates. Dirty updates do not
// derive DOM changes from this result; update mutable attributes explicitly with
// [Element] methods such as [Element.SetAttr] and [Element.RemoveAttr], or replace
// the Element. An empty string contributes no attributes.
//
// Callers must not hold a lock protecting the handler or its source. The method
// must synchronize shared state. Its result need not share a state snapshot with
Expand Down
105 changes: 76 additions & 29 deletions examples/minesweeper/AI.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,39 +6,80 @@ See the [module-wide AI guidance](../../AI.md) before changing this example.

The demo is a server-driven JaWS application with no custom client-side state.
One `game` is created in `run` and shared by all visitors, making the running
demo intentionally collaborative. Create request- or session-owned games in
the page handler if that product behavior changes; do not silently turn the
existing shared game into per-user state.
demo intentionally collaborative. For per-user state, wrap the page in an outer
handler that loads the user's game and invokes a freshly constructed
`ui.Handler` for that request. Put `jw.SessionMiddleware` outside that handler
when the lookup uses a JaWS Session. Do not silently change the existing
shared-game behavior.

The board shape and cell pointers are fixed after construction. Mutable game
and cell fields are protected by `game.mu`. Rendering takes an immutable
`cellView` snapshot while holding the lock, releases the lock, and then derives
trusted cell markup and queues Element presentation updates from that snapshot.
Keep state mutations out of getter/render paths.
and cell fields are protected by `game.mu`. Cell getters lock the game and read
those fields directly. Do not add a detached render DTO or parallel presentation
state: `game` remains the sole application model. `cell.Button` constructs a
fresh specialization that embeds the standard `ui.Button`; the shared pointer
is the authoritative cell, not copied UI state. Its promoted standard render
uses the cell as its getter, event source, and precise tag, while the template
passes the result of `cell.InitialAttrs()` as an ordinary parameter. Its
specialized update captures that one Button's current attributes and content
under the game lock, unlocks, and then queues Element changes. Keep state
mutations out of getter/render paths.

`run` deliberately constructs the application inline and injects only
`listenAndServe`. Preserve that copyable layout unless a production behavior
requires another seam.
`run` constructs the application inline and injects only `listenAndServe`.
Preserve that copyable layout unless a production behavior requires another
seam. The long-running server must configure the JaWS logger before setup so
update-time failures are reported instead of panicking. Tests that construct
JaWS directly deliberately leave it nil so illegal tags and other
framework-contract violations fail fast. Start `Serve` before exposing the
handlers, and do not add session middleware while the application has no
session-owned state.

## Dirty targeting

- A `Cell` is its own precise tag. `Cell.JawsGetTag` must return only the cell.
- Every cell button separately registers `Cell.BoardTag`, which is `&g.cells`.
This lets `Dirty(cell)` update one cell and `Dirty(&g.cells)` refresh the
complete board.
- Do not return the shared board tag from `Cell.JawsGetTag`. Tag expansion would
- A `cell` is its own precise tag. `cell.JawsGetTag` must return only the cell.
- The template passes `cell.BoardTag` (`&g.cells`) and `cell.GameOverTag`
(`&g.gameOver`) to every cell Button. This lets `Dirty(cell)` update one cell,
`Dirty(&g.cells)` refresh the complete board, and `Dirty(&g.gameOver)` update
both status and the terminal presentation of every cell.
- Do not return the shared board tag from `cell.JawsGetTag`. Tag expansion would
turn every single-cell action into a full-board update.
- Scalar status dependencies use the addresses of the exact `game` fields.
Mutations snapshot scalar state before changing it, then `changedTags` emits
only fields whose values differ afterward. HTML-inner widgets do not perform
application-level diffing, so broad scalar dirtying causes needless DOM work.
- A loss, win, or reset changes many cells and uses the shared board tag. Normal
reveals return the individual cells reached by flood fill, and flag toggles
return only the affected cell plus changed scalar fields.
Each mutation appends a field tag exactly when it changes that field.
HTML-inner widgets do not perform application-level diffing, so broad scalar
dirtying causes needless DOM work.
- Loss, win, and any reset that changes cell state use the shared board tag.
Normal reveals return the individual cells reached by flood fill, and flag
toggles return only the affected cell plus changed scalar fields.

The committed `BenchmarkSingleCellDirtyFanout` guards the targeted-update
design. Keep it when changing cell identity or tag registration, and verify it
still resolves a single-cell action to one cell Element.
Dirty tags carry dependency identities, not rendered values. `Request.Dirty`
schedules matching Elements across live Requests, and JaWS may batch or coalesce
those updates. Each scheduled Element re-reads the authoritative game state. A
dirty event can select only dependencies an Element has already registered; each
later matching event brings every registered copy current, including any cell
whose separately locked initial attribute and content reads straddled a mutation.
Mutations must not push a second representation of the game into UI objects.

The template owns the static `cell` class. Dynamic styling uses one
`data-state` attribute, so updates preserve caller-supplied classes. This is the
deliberate reason for the Button overload: without the specialization, the
standard Button would call its HTML getter during both initial rendering and
dirty updates. Queueing wrapper changes there adds 300 redundant DOM operations
to `TailHTML`. The specialized update keeps the initial tail empty while retaining
the standard Button render path. `BenchmarkInitialPageAndTail` is the regression
guard for that measured payload improvement.

Initial attributes come from the ordinary `cell.InitialAttrs()` template
argument; the cell does not implement `JawsInitialHTMLAttr`. Do not turn that
framework hook into an update callback. Its `template.HTMLAttr` result is trusted
opening-tag syntax: by itself it identifies neither removals nor attribute
ownership, and initial duplicate handling is not equivalent to live DOM updates.
A reusable dynamic-attribute API would need an explicit ownership/diff contract,
preferably structured set/remove operations, and belongs in a separate framework
change.

`TestSingleCellDirtyStaysScopedToOneCell` guards the targeted-update invariant.
The committed `BenchmarkSingleCellDirtyFanout` measures tag expansion and
cell-Element lookup cost while intentionally excluding the separate Stats
update. Keep both when changing cell identity or tag registration.

## Domain behavior

Expand All @@ -49,18 +90,24 @@ mine count to at least one and below the cell count. The game ends when a mine
is revealed or every safe cell has been revealed; both terminal states reveal
the mines and refresh the board.

The static `template.HTML` fragments in `cellView.HTML` contain only fixed
markup plus an integer adjacency count. Do not interpolate user-controlled
content into those trusted fragments.
Click reveals a cell. Context-menu and Shift-click events toggle its flag, so
the same semantic Button supports ordinary and modifier-assisted activation.

The trusted HTML returned by `cell.htmlLocked` consists only of fixed markup and
an integer adjacency count. Do not interpolate user-controlled content into it.

## Testing responsibilities

- Keep pure domain tests for construction bounds, first-click safety, mine
placement, adjacency, flood fill, flags, win/loss, reset, and no-op guards.
- Keep UI integration tests on real JaWS Elements for tag registration, event
dispatch, queued class/attribute updates, and exact dirty fanout.
dispatch, queued attribute updates, an empty initial TailHTML, and exact dirty
fanout.
- Use at least two live Requests to verify collaborative narrow updates and
board-wide refreshes.
- Keep the HTTP wiring test for templates, static assets, middleware, and route
setup without binding a real port.
- Run `go test -race ./examples/minesweeper` and a plain
`go test ./examples/minesweeper` from the module root. Run the benchmark with
`-bench=SingleCellDirtyFanout -benchmem` when changing dirty-target behavior.
`-bench=SingleCellDirtyFanout -benchmem` when changing dirty-target behavior,
and `-bench=InitialPageAndTail -benchmem` when changing initial rendering.
Loading
Loading