Skip to content

Surface hosting protocol: any node that offers a surface can host any placeable asset - #881

Open
wass08 wants to merge 20 commits into
mainfrom
feat/surface-hosting-protocol
Open

wass08 wants to merge 20 commits into
mainfrom
feat/surface-hosting-protocol

Conversation

@wass08

@wass08 wass08 commented Sep 16, 2026

Copy link
Copy Markdown
Collaborator

What

Hosting was the one layer of the editor that never became data-driven. Six declarations answered "who can host whom", two movers hardcoded the answer per kind, and the generic service in core/src/services/hosting.ts had five of its six functions with zero production callers. The visible symptoms were a procedural item that could not sit on a shelf, and a kitchen countertop that hosted nothing at all, for any asset type.

Surfaces are now first-class and addressable, with two providers. Declared surfaces come from nodes that know their own geometry: shelf boards, countertop spans, bar ledges, a recipe's named surfaces. Hit-derived surfaces come from the raycast for every other mesh, which is how a book still lands anywhere on a sofa armrest. Absence of a declaration means freeform and allowed, never unhostable.

Behaviour

  • Shelves, countertops and islands hold things, for catalog items and generated designs alike, through both movers. Counters elect no sub-surface, so you slide freely along them; the bar ledge is its own surface at its own height; sink and hob cutouts refuse; wall and tall units publish nothing so a kettle cannot land on a pantry.
  • A generated design holds things anywhere on it. Where its recipe names a surface, the child stores the surface id and a surface-frame pose and survives the design being resized. Otherwise the surface comes from the pointer hit and the child stores a pose only.
  • Fit follows the world, not a rectangle. Something stays hosted while its centre is on the surface, so overhang is free and it lets go only past halfway. Nothing is clamped or snapped back.
  • Hosted movement makes the same sound as the ground, through one shared tick, without doubling when a grid event and a host event arrive together.
  • hostable.parents is gone as an eligibility gate; the host's surface decides.

Fixes found on the way

  • Procedural items were never a persisted node kind, so the scene API used by agents rejected any scene containing one and collaborative projects refused the node outright.
  • A runtime import cycle appeared when that was fixed, and is broken by extracting the pure footprint helper.
  • Recipes could not express whole-number division, so every generated design using an indexed grid put a stray part outside its bounds; a four-legged table with a stated width of 450 mm measured 655 mm. Adds floor, ceil, round, abs and mod.
  • A dragged mesh floated above its own preview box whenever a host stood on a raised slab or was itself hosted; 52 combinations were wrong.
  • Editing a slab deleted everything standing on it. Removing the host allowlists made slabs reachable as hosts, which released main does not allow, and a slab's schema has no children field so every update stripped the link. Slabs defer to floor placement again, and an update that omits children now carries them forward, closing the same hole for 24 other kinds.

Checks

Editor 1032, core 2807, nodes 3110, viewer 341, host and preview matrices 406, frozen behaviour table 720. Root typecheck 12/12, Biome clean, zero skips in core and editor.

The frozen table records 717 cases across three adapters with per-adapter guards asserting both accepted and refused verdicts are represented, after a section once collapsed to a single verdict and hid two real defects.

Browser-verified on a live scene: a procedural item on a shelf board with row switching, gap stickiness, reload and undo; catalog items on counters with the countertop proven not to rebuild across ~14k sampled frames.

Not in this PR

2D floor plan hosting (acquire, retain, cycle) and the remaining adapters for column tops and block faces. Tracked in plans/editor-surface-hosting-protocol.md in the private repo.

🤖 Generated with Claude Code

https://claude.ai/code/session_013LKpG6PpZe6Jb4dBBAKtBG


Note

High Risk
Touches scene graph validation, hosting/placement, and floor/slab elevation paths across many node kinds; incorrect surface or children handling could drop hosted nodes or mis-lift geometry on save/load.

Overview
Introduces a surface-based hosting model where eligibility comes from declared or hit-derived surfaces (shelf boards, countertops, recipe surfaces, mesh raycasts) rather than hardcoded parent-kind lists. hostable.parents is removed from canAttach, registry gains surfaces.hosting / SurfaceProvider, and fit policy tests center-based placement with overhang allowed.

Procedural items become a first-class AnyNode kind (ProceduralItemNode in the discriminated union via safeExtend), with persistence/round-trip tests and a browser bundle guard so schema imports do not pull in scene/spatial-grid runtime.

Refactors pure geometry helpers into floor-placed-footprints.ts and item-polygon-overlap.ts to break import cycles; procedural query.ts gains slab lift for shelves/cabinets (multi-footprint for cabinets), skips unmapped procedural children in attachment validation, and uses surfaceRegionContainsPoint for hosted fit checks.

Host children schemas widen to string[] (plus column.children) so shelves, blocks, cabinets, and items can list procedural/plugin children while frozen corpus tests lock backward compatibility.

Recipe expressions add floor, ceil, round, abs, and mod (with tests and a 2×2 leg grid-table fixture) so indexed part layouts stay within nominal bounds.

Reviewed by Cursor Bugbot for commit 04eb64f. Bugbot is set up for automated code reviews on this repo. Configure here.

wass08 and others added 20 commits September 15, 2026 16:15
Adds the surface descriptor, the two providers and the pure placement
resolver behind the surface hosting protocol. Declared surfaces come from
nodes that know their geometry; every other mesh resolves through a
hit-derived surface with no durable id, which is how a book keeps landing
anywhere on a sofa armrest.

Permissive by default: acceptance only restricts when a provider declares
it, and the single restriction is a list of non-physical host kinds.

Additive only. No mover, node definition or schema changes, and the tests
pin parity by executing the existing item, shelf and procedural helpers
rather than restating their expected output.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013LKpG6PpZe6Jb4dBBAKtBG
An independent audit found three blockers. The module was never exported
from the package barrel, so no mover could have imported it and the first
consumer was forced to reach into core's source. Declared attachments are
stored in the surface's own frame while items and shelves store host-local
poses, and the resolver returned only the latter, so the slice meant to put
a procedural item on a shelf could not have written its result. And every
test stayed green when the surface rotation maths was deleted.

Providers now declare which frame their children live in and the resolver
returns both poses. The snap flag becomes an explicit grid-snap boolean,
since grid snapping is universal here and the old flag centred nothing
while ignoring its own free setting. Rejections return nothing at all,
matching both legacy paths, with an opt-out for the footprint check because
dragging skips it today.

The parity table is frozen alongside the live comparison so it survives the
slice that moves the code it pins.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013LKpG6PpZe6Jb4dBBAKtBG
A kitchen run now advertises the surfaces something can actually rest on:
one per real countertop span and one per bar ledge at its own height, with
sink openings, faucet bases and cooktops as holes. Wall runs, tall runs and
empty runs publish nothing, so a kettle cannot land on a pantry.

Surface ids are anchored to a module rather than a position, so inserting a
module elsewhere in the run does not move something already resting on the
counter. The span and appliance geometry is extracted so the builder and
the surface derivation read the same numbers and cannot drift.

Nothing consumes this yet. Declaring it changes no behaviour.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013LKpG6PpZe6Jb4dBBAKtBG
… service

Surface eligibility, height, grid snapping and footprint fit now come from
the shared resolver instead of the mover's own copy. The mover keeps what is
genuinely its own: self and descendant rejection, the grab offset, the frame
conversion, the scene writes and the session lifecycle.

Behaviour is unchanged by construction. Every existing registry test passes
untouched, and the footprint check still runs on enter and move rather than
at commit, which is this mover's own rule and not the catalog mover's.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013LKpG6PpZe6Jb4dBBAKtBG
…egy adopts the resolver

Four files were computing the same world-space surface normal, and adopting
the resolver in a second mover would have made a fifth. One adapter now turns
a host and a pointer event into the resolver's hit, and both movers use it.

The catalog item-surface strategy resolves through the shared service. Its
timing is preserved: entering checks the footprint, moving deliberately does
not. The registry mover picks its write frame from the provider's declared
childFrame rather than sniffing the shape of the node's rotation field.

One deliberate difference from legacy: a non-finite hit is now rejected
rather than producing a node at a NaN position. That path was reachable only
for a host with an authored surface height, and its old result was corrupt.

The roof strategy keeps its own normal because it needs the full vector for
tilt, not just the upward component.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013LKpG6PpZe6Jb4dBBAKtBG
… the parents gate

Shelf row election, grid snapping and the entry fit check now come from the
shared resolver and the shelf provider. Stickiness stays where it belongs, in
the coordinator, which keeps hosting while the cursor ray is inside the shelf
volume even as board-gap leave events arrive.

canAttach no longer enforces a child's declared parent list. That gate was
never reached by a pointer path, and wiring it up later would have quietly
made procedural items more restricted than they are today. Its self-host,
cycle and depth checks remain, because those are real.

The field itself survives for now: the registry mover genuinely reads it to
decide whether to open an item-surface session. Retiring that reader needs its
own replacement and is tracked in the plan.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013LKpG6PpZe6Jb4dBBAKtBG
…he legacy resolver goes

The parity tests used to pull mover helpers out of source at runtime and run
them beside the new code. One shared flag gated every comparison, so deleting
two shelf helpers silently skipped 39 tests across all three adapters and the
suite still reported green. A silent skip reads exactly like coverage.

The frozen table is now the pin: 431 recorded cases across item, shelf and
procedural, up from 328 with procedural previously unrepresented. Each adapter
asserts its own case count, so quietly dropping cases fails instead of shrinking
into a smaller green run. Deleting one case fails one test; shifting item height
by a tenth fails twelve.

With the extraction gone, the legacy resolver and its private helpers had no
callers left and are deleted rather than kept alive by the tests that measured
them.

Core and editor now skip nothing at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013LKpG6PpZe6Jb4dBBAKtBG
Procedural items shipped to production but were never added to the runtime
node union, only to the TypeScript type. Two things broke for real users: the
scene API that agents and MCP use rejected any scene containing one, and
collaborative projects rejected the node outright, so a procedural item could
not be placed in a shared project at all.

An item hosting a procedural child also failed to parse. That path is reached
on every update to such a host and on community wall merging; it failed
silently and fell back to an unvalidated candidate, which is why the child kept
rendering and nothing looked wrong. Items are now widened the same way wall and
ceiling already were.

Blocks are deliberately left alone: their face attachments need an identifier a
procedural node does not carry.

Strictly additive. All 48 existing node kinds still round-trip, and scenes
without procedural items validate exactly as before.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013LKpG6PpZe6Jb4dBBAKtBG
They were registered separately by the community app instead of belonging to
the builtin plugin like every other kind, which is the same oversight that left
them out of the persisted node union.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013LKpG6PpZe6Jb4dBBAKtBG
Every host typed its children list to one kind by id prefix, so each new host
and child pairing needed a core schema edit and a plugin kind could never be
hosted at all. Shelf, cabinet, cabinet module, block and item now take plain
node ids, and column gains the field so it can host without a second pass.

The schema stops being the gate. What a host accepts is decided by its surface
and its accept predicate, which is the thing that actually knows. Cycle limits,
depth limits and id format are untouched.

A shelf can now hold a procedural item and a cabinet can hold an item, at the
core schema, the scene API and the collaboration contract. All 50 existing node
kinds round-trip unchanged and all eight previously allowed pairings still
validate.

One deliberate tightening: a column carrying a malformed children value, such
as a number, used to be ignored and now fails validation. That was never valid
data.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013LKpG6PpZe6Jb4dBBAKtBG
The original report. The mover that handles procedural items subscribed only
to item events and refused any host that was not an item, so it never entered a
shelf state at all: it kept tracking the floor and a click committed there or
refused as a collision.

Shelf events now route through the same resolver the catalog mover uses, so row
election, grid snapping, the fit check and the shelf-local yaw are the shelf's
one implementation rather than a second copy. Both movers now share the
stickiness rule, so a design stays hosted while the cursor is still inside the
shelf even as leave events arrive over the gaps between boards.

A shelf standing on a raised slab lifts the child once, not twice: attaching
clears the child's own support and detaching re-elects it. Moving or resizing
the shelf cascades dirty marks to procedural children, and attaching one still
does not churn the shelf geometry key.

Wall and ceiling mounted designs keep their own session and never enter a shelf.
In plan view a shelf-hosted design draws correctly; dragging behaves exactly as
a catalog item does today, which is its own slice.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013LKpG6PpZe6Jb4dBBAKtBG
Both movers now route cabinet events through the shared resolver and the shared
stickiness rule, so a catalog item and a procedural item can each sit on a
counter. A counter elects no sub-surface: you slide along it freely, with the
grid snapping that applies everywhere else, and the bar ledge is reachable as
its own surface at its own height. The child parents to the run that actually
owns the surface it was dropped on, including a nested corner leg.

An upward hit is not a fit, so the rotated footprint is validated after snapping
against the real span, minus sink openings and appliance regions.

The known trap is handled generically rather than by special-casing cabinets: a
host now declares which child kinds affect its geometry, and both the geometry
key and the viewer's live override key honour that. Attaching or dragging
something on a counter no longer rebuilds the countertop, which would have made
the pointer flicker. Structural module changes still rebuild it, as they must.

In plan view a counter-hosted child draws correctly and keeps its host while it
stays within it. Acquiring a host in 2D remains its own slice.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013LKpG6PpZe6Jb4dBBAKtBG
…port cycle

Two protocol defects survived the earlier slices. The registry mover still
required a child to declare a hostable opt-in, and it still checked host kinds
against a hardcoded list. Both are exactly what this plan exists to delete:
eligibility now comes from whether the host offers a surface that accepts the
child. A design that declares nothing now hosts on all four surfaces.

Adds the host matrix test that should have existed: both movers against an item
top, a shelf board, a countertop and a bar ledge. The previous tests passed
because their fixtures declared the opt-in and used known hosts, so they could
not see either defect.

Also breaks a real import cycle. Making procedural items a persisted kind gave
the schema a runtime dependency on the procedural query module and everything it
pulls in, which reproduced as an initialisation error in a clean bundle.
Extracting the pure footprint helper removes the cycle with no change to
validation and no lazy loading.

This does not yet explain the browser report that a procedural item ignores a
countertop; the ordinary fixture passed before these fixes too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013LKpG6PpZe6Jb4dBBAKtBG
…s stop coming out lopsided

The expression language had only add, subtract, multiply, divide, min and max,
and divide is floating point. The model writes the standard idiom for a two by
two arrangement, dividing a part index by two and expecting it to round down,
and gets fractions instead. A four-legged table put its fourth leg outside the
tabletop and bunched the rest to one side, making a design whose stated width
was 450 mm actually 655 mm, 46 percent larger.

Diagnosed in a running browser: the evaluated bounds matched that arithmetic to
the digit, and the oversized result is why the design would not fit a standard
600 mm counter while an equally oversized catalog chair was refused identically.

Adds floor, ceil, round, abs and mod. Floor rounds toward negative infinity,
mod returns a non-negative remainder and refuses a zero or negative divisor, and
the evaluation budget still applies.

Existing recipes using plain division are left exactly as they are, since
fractional spacing can be deliberate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013LKpG6PpZe6Jb4dBBAKtBG
The snap tick fired on the ground, walls, roof wall faces, block faces and
ceilings, and nowhere else, so moving something across an item top, a shelf
board, a countertop or a bar ledge was the only silent movement in the editor.
A procedural item went quiet the moment it attached to anything.

Both movers now tick through one shared helper on the same step rule, so hosted
movement is indistinguishable from moving on the floor. Native event identity
suppresses the duplicate tick when a grid event and a host event arrive for the
same physical movement, which would otherwise have doubled the cadence on a
counter. Muting and throttling are unchanged, and the place sound on commit is
untouched.

A generated design used as a host is not fully covered yet: the catalog route is
missing and the registry route needs a named surface selected first.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013LKpG6PpZe6Jb4dBBAKtBG
…fusals that say why

The fit test behaved two ways. A counter published its size, so the child's
rotated hull had to sit inside it. A shelf board published nothing, so the check
fell back to the whole shelf and ignored rotation entirely. The same design was
refused by a 600 mm counter and simultaneously accepted by a board it visibly
overhung.

Strictness now follows what a surface knows about itself. Shelf boards publish
their real rectangles, counters publish spans with the sink and hob holes, and
generated designs publish their evaluated size; a declared surface is required
by the type to carry an extent, so no future adapter can slip onto the lenient
path. Arbitrary geometry like a sofa keeps a best-effort check but stops
ignoring rotation.

200 recorded shelf cases flip from accepted to refused. Each was a genuine
overhang, the smallest around 68 mm. Because those were every acceptance the
shelf fixtures had, 286 contained placements were captured alongside them, and
each adapter now asserts it holds both verdicts so a section can never collapse
to one unnoticed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013LKpG6PpZe6Jb4dBBAKtBG
… surface

Two earlier attempts got this wrong. Requiring the whole footprint inside the
surface was too strict, and sliding an object back on when it neared an edge was
worse, because it takes control away from the person dragging.

The rule is now the one that matches the world: overhang freely, and let go only
once more than half the object is off, which is when its centre leaves the
surface. A mug can sit half off a table and stay there. Nothing is nudged,
clamped or snapped back. A child larger than the surface is placeable while its
centre rests on it.

Holes still refuse, so a centre over a sink or hob is not a placement, though
overhanging across one is fine.

The refusal wording is gone; near an edge there is nothing to read. The
machine-readable reason stays for tests and diagnosis, and the guard against
silently committing to the floor at a different position stays, since that was
the one genuine defect behind the wording.

All 717 recorded cases were recaptured: 248 move from refused to accepted and
none the other way, which is the expected direction for a more permissive rule.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013LKpG6PpZe6Jb4dBBAKtBG
A generated design already declared that it accepts items and other designs, but
a child could only attach to a surface the recipe explicitly named, and most
generated recipes name none. So in practice nothing could rest on one, which is
what the owner reported.

It now has the same fallback a sofa has. Where the recipe names a surface and
the hit lands on it, that path wins unchanged: the child stores the surface id
and a pose in the surface frame, so it survives the design being resized.
Otherwise the surface comes from where the pointer actually touches the shape,
and the child stores a pose only, with no durable surface identity it never had.

The catalog route needed real work rather than the wiring being assumed: its
event routing and attachment persistence for this host were both missing.

Wall and ceiling mounted recipes stay out; they are not floor-resting hosts.
Hosted movement on a generated design now ticks like every other surface.

The host matrix covers all six surfaces across both movers and both child kinds,
including a design that declares no opt-in at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013LKpG6PpZe6Jb4dBBAKtBG
Dragging a catalog item onto a generated table drew the green bounds box on the
tabletop while the mesh hovered a table's height above it, snapping into place
only on release.

The floor elevation system was copying a hosted preview's world-space height
into the mesh's local height, so the host's surface height was applied twice.
Elevation writes are now restricted to nodes parented to the level, which are
the only ones that should receive a floor lift. The attachment bookkeeping and
the procedural surface groups were correct all along.

This was never specific to generated designs. 52 combinations were wrong:
every host type when standing on a raised slab or itself hosted, plus generated
named surfaces even at ground level, which is why it showed up there first.

The durable fix is the matrix that was missing: 324 cases mounting the real
movers, renderers and frame systems, asserting that the preview mesh, the
preview box and the committed transform agree to six decimal places across
entry, movement, rotation, fresh and existing placements, and composed host
frames. It fails 52 before this change and passes 324 after.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013LKpG6PpZe6Jb4dBBAKtBG
A stack resting on a slab vanished when the slab's surface, base or thickness
changed by any amount, down to a 2.5 mm nudge. Not culled or sunk: the subtree
unmounted, autosave then stored the slab without its child list, and after a
reload those nodes survived in the saved graph as orphans nothing pointed at.

Two independent faults, both fixed.

Slabs were never meant to be hosts. The editor's model is that something on a
slab is a level child carrying a supportSlabId, lifted by the elevation system,
and there is a comment warning that parenting to a slab corrupts the parent and
child bookkeeping. Removing the hardcoded host allowlists on this branch made
slabs reachable as hosts, which released main does not allow. A slab hit now
defers to floor placement, so the node stays a level child as before.

Separately, a slab's schema declares no children field, and node updates return
the parsed object, so zod stripped the key and severed the link on every write.
An update that does not mention children now carries the existing ones forward,
which closes the same hole for 24 other kinds that can be hosted onto and also
lack the field.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013LKpG6PpZe6Jb4dBBAKtBG
@pascal

pascal Bot commented Sep 16, 2026

Copy link
Copy Markdown

I hit an error while handling your request (Model unavailable on AI Gateway free tier: Free tier users do not have access to this model. Upgrade to paid credits at https://vercel.com/d?to=%2F%5Bteam%5D%2F%7E%2Fai%3Fmodal%3Dtop-up for unrestricted…).

Please try again, rephrase, or reach out if it keeps failing.

Error id: 94fb1019-a079-4dc9-be27-d814ba2acb4b

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 04eb64f. Configure here.

(host as { recipe?: { mounting?: { attachTo?: string } } }).recipe?.mounting?.attachTo !==
'ceiling'
)
return attachTo !== 'ceiling' && !(host as { recipe?: { mounting?: unknown } }).recipe?.mounting

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mounted designs cannot host items

Medium Severity

canHostOnTop now treats any truthy recipe.mounting as ineligible, not only ceiling attachment. Wall-mounted generated designs are rejected as host-not-eligible before their named surfaces run, while catalog items with attachTo: 'wall' still host. The comment still describes a ceiling-only gate.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 04eb64f. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant