Skip to content

Applications: web form schema, sealed web intake, public-profile consent - #74

Open
robertovalenti wants to merge 284 commits into
mainfrom
claude/application-schema
Open

robertovalenti wants to merge 284 commits into
mainfrom
claude/application-schema

Conversation

@robertovalenti

Copy link
Copy Markdown
Member

Brings HolonsBot's application in line with the new web form at liminalvillage.com/apply, and gives web applications a sealed home in Holosphere.

What changes

  • core/applications (new domain): the web application record (parseWebApplication), sealed-only storage (ensureApplicationsSealed: the lens is locked with @holons/core/privacy before any write; nothing is ever written in plaintext), and the public-profile consent (PROFILE_KEYS, profile claims).
  • telegram-ui/booking: data/booking.json asks the same questions as the web form (multi-role, in-person / unsure / remote, referral, X / LinkedIn / website, optional transport and accommodation). BookingSystem gains a declarative showIf ({field, in}, {field, notIn}, {allEmpty}), so remote applicants skip the travel questions. Select answers are now stored as values plus <key>_other text (previously {value, customValue} / other:<text>).
  • telegram-ui/server: POST /applications (bearer APPLICATIONS_TOKEN) stores web applications in APPLICATIONS_HOLON (default Liminal Village, -1003864542239), lens applications, sealed to the holon's members. 503 if the token is unset or the lens can't be sealed; idempotent per id.
  • Public profile: with consent, only ikigai + LinkedIn are written to the applicant's DNA record, via t.me/HolonsBot?start=profile_<claim> for web applicants.

Deploy notes

  • Set APPLICATIONS_TOKEN on the bot (the same value is already set on the liminalsite Netlify project) and optionally APPLICATIONS_HOLON.
  • Run pnpm -F @holons/core build before restarting: the bot imports core from dist/.
  • The first web application locks the lens (slower: keys go out to members). To do it ahead of time, a chat admin can send /lock applications in the Liminal Village group.

Tests

  • core applications: 23/23. telegram-ui: 169 passed, 1 skipped. Typecheck and lint clean for core and telegram-ui. In a fresh checkout pnpm build is needed before the full gate.
  • Not exercised against live relays or the real holon: whether the bot's key can lock the lens there is unverified; if it can't, the endpoint answers 503 and writes nothing.
  • Telegram /apply still saves to the applicant's personal holon unsealed (unchanged); only web applications are sealed.

The dock grows a Deck ⇄ Map toggle (persisted per device). The map view
places every docked board on its claimed cell (`settings.hex`, the field
the HexPicker writes) on the Mapbox basemap, drawn in TWO hexagon
layers the way the dashboard map does: the faint dashed parent
neighbourhood (5 resolutions coarser, findable from country zoom) under
the exact cell. Tapping a parent hexagon flies to its board's cell at
the resolution-appropriate zoom (goToHex-style); the hexagon badge at
the cell carries the same data-dock-circle handle the deck's orbs do,
so tapping it opens the board through the same morph — and closing a
board in map mode irises back down onto its hexagon (the morph target
lookup now waits briefly, since badges land after the basemap loads).
The basemap dissolves into the space with a wequest-style edge fade.
Claimed cells are cached across mounts; boards without one stay
deck-only and the map hint says where to claim a place.

Geolocation is now asked ONLY on an explicit My-location tap: the map
view gets its own ◎ button, the HexPicker no longer auto-locates on
open, and the sunset theme no longer prompts at boot — it reads the fix
those buttons cache (setGeo), falling back to fixed evening hours.

Verified headlessly: 0 permission asks at boot, exactly 1 after tapping
◎; badges open/close boards through the morph in map mode; a parent
tap flew z13 → z15 onto the exact cell; toggle round-trips to the deck
with orbs and vesica intact. 202 tests, typecheck and lint green.

Claude-Session: https://claude.ai/code/session_01BMAzedDQ6m7xGgRujPQMzG
Signed-off-by: Roberto Valenti <robbyvalenti@gmail.com>
The map view gains the zoom-following H3 grid it was missing, drawn in
the dashboard map's two layers — the zoom-matched resolution plus the
next finer one fainter inside it — rebuilt on every move/zoom with a
cell-count cap. All hexagon lines are thicker, and cell rings apply the
dashboard's antimeridian hack (shift the positive side by -360° when a
boundary jumps) so cells straddling ±180° no longer paint lines across
the world.

The Deck ⇄ Map switch now hovers over whichever surface is showing
(frosted pill, top centre) instead of sitting in the tray; the tray
keeps "+ Add a board" at the bottom in BOTH views, with the per-view
hint. The map gets the HexPicker's Mapbox geocoding search as a
floating pill under the switch — pick a hit and the map flies there.

Verified headlessly: 730 grid cells rendered in a city viewport,
searching "Milano" flew the map to 45.46,9.19, toggle/add/hint placed
as designed in both views. 202 tests, typecheck and lint green.

Claude-Session: https://claude.ai/code/session_01BMAzedDQ6m7xGgRujPQMzG
Signed-off-by: Roberto Valenti <robbyvalenti@gmail.com>
h3's polygon fill returns nothing for a viewport wider than half the
globe — exactly the fully zoomed-out view — so the grid and the lens
presence subscriptions vanished. `viewportCells` now serves world-wide
boxes from the global cell set, re-wraps longitudes Mapbox unwraps past
±180, and keeps latitudes off the poles.

The map sits in a rounded, dark-grounded card with an inset vignette,
basemap edge to edge; the hex grid carries a per-cell `fade` so it
dissolves before the rim (edgeFade) while lit lens cells stay solid.

Claude-Session: https://claude.ai/code/session_01WbVfqjbrT8288kHxxZmKuH
Signed-off-by: Roberto Valenti <robbyvalenti@gmail.com>
…n, place search

Companion to 91db231 (which shipped DockMap + maplens but not the modules
they import — this restores a buildable HEAD):

- holosphere: subscribeLensPresence, the dock map's per-cell presence
  channel. Gun replays a lens's records to the FIRST subscriber only, so
  the channel taps every record it hears (onItem) for the cell panel to
  seed from; graph-metadata junk (HAM fragments under ">", soul refs) is
  filtered via looksLikeRecord so it neither lights a cell nor reaches the
  panel. normalizeSub exported for the panel's own live subscription.
- i18n: the 18 dashboard lens labels + map panel strings (en/it/es).
- DockView: place search extracted into PlaceSearch.svelte; orbs sized in
  rem measured from the live root font so the vesica matches the drawn
  circles at every viewport width.
- layout: the open/close morph waits briefly for the map badge to mount
  before animating, instead of jumping when the basemap lags.

Verified headlessly against HolonsDebug: seeded quests light exactly their
cell, tapping it lists them in the panel; junk-lit cells are gone.

Claude-Session: https://claude.ai/code/session_01BMAzedDQ6m7xGgRujPQMzG
Signed-off-by: Roberto Valenti <robbyvalenti@gmail.com>
…hexagon

The post-it cards hash a seed into the six-note palette (a task by its
category, a role by its title). The same hash applied to the holon id now
picks the holon's colour, and that one colour is used everywhere the holon
appears: a faint wash over its board card, its orb in the dock, its
hexagon (cell, parent cell, marker) on the map, and the glow edge of every
card mirrored from it via federation or a hologram. The dock's separate
hue hash is gone.

A caretaker can override it with a `color` field on the holon's settings
document. Core owns the algorithm and the override rule
(core/settings/color.ts: colorHash, pickColor, holonColor, readHolonColor,
saveHolonColor, spec beside it); HolonSettings carries the optional field.
The kiosk keeps learned overrides in lib/palette (displayed holon, its
partners, every docked board) and Settings gains a per-holon "Board colour"
picker with an Automatic reset (en/it/es).

Claude-Session: https://claude.ai/code/session_01WbVfqjbrT8288kHxxZmKuH
Signed-off-by: Roberto Valenti <robbyvalenti@gmail.com>
… to record details

Top bar: search | Deck⇄Map switch | My-location — the switch stays centred
whatever the side slots hold; the locate button moves up from the map's
bottom corner (DockMap exports locate()) and the lens chips take the whole
bottom edge.

Cell panel: each listed item is now a tap-through to a detail card read
straight off the record's JSON — well-known fields first (status, when,
where, amount+currency, participants by name, link, origin holon…), dates
in the kiosk locale, coordinates as lat/lng, then the remaining scalar
fields; graph plumbing, pictures and headline echoes are dropped.
itemDetails() lives in lib/maplens with tests; labels in en/it/es.

Claude-Session: https://claude.ai/code/session_01WbVfqjbrT8288kHxxZmKuH
Signed-off-by: Roberto Valenti <robbyvalenti@gmail.com>
On the map view the bar is inset to the card's margin plus the lens
chips' breathing room, so the search and the My-location button never
overhang the rounded frame. Under 40rem the switch keeps the top line and
search + locate wrap onto a second line, the search taking the width the
button leaves.

Claude-Session: https://claude.ai/code/session_01WbVfqjbrT8288kHxxZmKuH
Signed-off-by: Roberto Valenti <robbyvalenti@gmail.com>
…dDB and file adapters

Phase 1 of removing Gun: a standalone store that mirrors signed kind-30078
events (records + per-author envelopes + backlinks + cursors), NIP-44
password lenses (scrypt key, frozen params), one LWW rule, and a change
feed with per-subscriber replay. Nothing uses it yet.

Also: mini-relay honours `limit` (newest-first) so pagination is testable;
nostr-tools becomes a hard dependency; fake-indexeddb as a dev dependency.

Claude-Session: https://claude.ai/code/session_01WbVfqjbrT8288kHxxZmKuH
Signed-off-by: Roberto Valenti <robbyvalenti@gmail.com>
…nly wire

Phase 2 of removing Gun. HoloSphere keeps the same public API but runs on
the event-sourced store: every write is a signed kind-30078 event applied
locally and published by the relay transport; reads are store lookups;
subscriptions ride the store's change feed (per-subscriber replay, no echo
storms, no quarantine, no reconnect heartbeat, no read/write deadlines).

- content/global/node/utils rewritten over the store; hologram redirect and
  cascade rules kept verbatim, backlinks replace the _holograms sub-nodes
- signing.js is network-free: envelopes come from the store's events table;
  enforce reads resolve pointer claims through the instance (cycle-guarded)
- relay-transport.js: store.apply ingest, persisted cursors, paginated
  backfill, warm since-catch-up, reconnect + reopen backoff, resync()
- private (password) lenses re-implemented with NIP-44 (private-ops.js)
- handshake NIP-17 only; registry/subscriptions shims over the store
- new API: listHolons/listLenses/listKeys/getBacklinks/exportEvents/importEvents;
  removed: gun/getGun/getNodeRef/putNode/userName/configureRadisk/
  getRadiskStats/getAllLegacy/rehydrate/migrateRelays/gunOptions/backend
- gun dropped from package.json; ws optional; browser bundle has no node: imports
- tests: testenv on the memory adapter + mini-relay; Gun-only suites deleted,
  the rest ported; new relay-sync suite; enforce-mode skips removed
  (suite green unsigned, shadow and enforce)

Claude-Session: https://claude.ai/code/session_01WbVfqjbrT8288kHxxZmKuH
Signed-off-by: Roberto Valenti <robbyvalenti@gmail.com>
…ore, no gun

core: `relays.ts` (DEFAULT_RELAYS, resolveRelays, parseSigningMode,
signingOptionsFor) replaces the backend switch; factory takes
{relays, store, signing, nostr}; relayBackup removed (the relay IS the
wire); task deletion reads backlinks through `getBacklinks`; federation
publish no longer registers raw-graph forwarders; "Gun merges" comments
corrected (put replaces the whole record).

kiosk / wequest: instance built with relays + IndexedDB store; peer/
backend env keys, Buffer polyfill and relay backup dropped; presence
treats a null emission as removal; junk-guard no longer knows Gun souls.

Claude-Session: https://claude.ai/code/session_01WbVfqjbrT8288kHxxZmKuH
Signed-off-by: Roberto Valenti <robbyvalenti@gmail.com>
…he legacy "Show all data" overlay

web: the layout builds HoloSphere through the core factory with relays,
IndexedDB store, signing mode and projections — no backend switch, no
raw-graph console repair helpers (__nuke / __repairCircular). Global holon
discovery uses `listHolons()`. The "Show all data" toggle only ever
revealed records stranded on the retired gun relay; it is gone from the
title bar, lensFilters, QueryManager and the seven lens views —
`_unverified` items are simply never shown. The calendar feed lambda
runs on the memory adapter. Buffer polyfill and relay backup removed.

holosphere: subscribeFederated loses its `includeLegacy` / `setLegacy`
overlay (getAllLegacy no longer exists).

Claude-Session: https://claude.ai/code/session_01WbVfqjbrT8288kHxxZmKuH
Signed-off-by: Roberto Valenti <robbyvalenti@gmail.com>
Single factory path: HOLOSPHERE_RELAYS (default production relays),
HOLOSPHERE_STORE_DIR for a warm file-backed store, HOLOSPHERE_SIGNING
read mode. holon_list enumerates via `listHolons()` instead of the raw
graph root; the peer URL, backend switch and radisk sleep are gone.

Claude-Session: https://claude.ai/code/session_01WbVfqjbrT8288kHxxZmKuH
Signed-off-by: Roberto Valenti <robbyvalenti@gmail.com>
… gun server, proxy and raw-graph reads

The factory builds HoloSphere with HOLOSPHERE_RELAYS (default production
relays), a file store under HOLOSPHERE_STORE_DIR and the read-side
signing mode; the backend switch, peer override, relay backup and
radisk inspection are gone. Boot awaits `ready()` instead of poking the
graph root. Onboarding scenes write a member's DNA through a small
`dna.js` module (`<user>/dna/<user>`) instead of raw graph puts; the
deep-link handler reads through `db.get`. Schema scripts publish public
global records. Deleted: GunServer, proxy, merge_gun_data, testAuth,
guntest.html, gun-sync/config tests; `gun` and `http-proxy` dropped.

Claude-Session: https://claude.ai/code/session_01WbVfqjbrT8288kHxxZmKuH
Signed-off-by: Roberto Valenti <robbyvalenti@gmail.com>
…e gun repair scripts

Root and core scripts build HoloSphere with relays + a memory store
(the registry backfill opens the bot's file store to enumerate holons)
and close the instance on exit. Importers sign with the importer key
and no longer need a relay backup or a scratch radata dir.
find-dangling-holograms, repair-circular-holograms and holon-health-check
only repaired damage specific to the gun graph and are removed.
discord-ui passes relays, a file store and the signing mode.

Claude-Session: https://claude.ai/code/session_01WbVfqjbrT8288kHxxZmKuH
Signed-off-by: Roberto Valenti <robbyvalenti@gmail.com>
Repo-wide grep gate passes (only CHANGELOG history and the incident notes
still mention gun). Docs describe the relay-only data layer: CLAUDE/AGENTS,
README, ONBOARDING, TUTORIAL, docs/architecture, docs/realtime-sync
(rewritten), holosphere NOSTR-BACKEND (rewritten), SIGNING, VIBE, README.
Gun-era comments across web/kiosk/core scripts/holosphere are reworded;
holosphere tests drop the unused gun option helpers and ignored backend
flags; radisk examples removed. deleteGlobal now calls deleteFunc (the
namespace import had no `delete`). .env.example loses the peer key, CI
runs on Node 22 with a HOLO_TEST_SIGNING=enforce leg, lockfile has no gun.

Claude-Session: https://claude.ai/code/session_01WbVfqjbrT8288kHxxZmKuH
Signed-off-by: Roberto Valenti <robbyvalenti@gmail.com>
Standard-kind projections (NIP-52 / NIP-99 / kind 0 / NIP-51 / NIP-58 /
NIP-29) are published for every lens by default on every surface:
`parseProjectionList(undefined)` now means "all", and the new
`projectionOptionsFor({ appName, privateKey, lenses })` in
@holons/core/nostr builds the `nostr` block for kiosk, wequest, web, the
Telegram and Discord bots and mcp-ui (which now generates its own key when
none is configured so projections share the signer's pubkey).
`HOLOSPHERE_PROJECTIONS=off` opts out.

The signing-mode switch is gone: `HOLOSPHERE_SIGNING` / `VITE_*_SIGNING`,
`parseSigningMode`, `signingOptionsFor`, the factory `signing` option, the
web read-key seeding and `__signingReport`, the kiosk `resolveSigningMode`,
the Netlify/CI knobs and the enforce read-list integration test. Every
write is signed by the instance key and published to the relays; shadow /
enforce remain library-only test modes.

Claude-Session: https://claude.ai/code/session_01WbVfqjbrT8288kHxxZmKuH
Signed-off-by: Roberto Valenti <robbyvalenti@gmail.com>
Nostr secret keys are now `*_NSEC` env vars and may hold NIP-19
`nsec1…` (64-char hex still accepted): HOLOSPHERE_NSEC,
VITE_HOLOSPHERE_NSEC, ATLAS_IMPORTER_NSEC. holosphere normalizes the key
at every entry point (constructor, login, enableSigning, nostr-events)
and @holons/core/nostr gains nsecToHex / toNsec / generateNsec, which the
bots, mcp-ui, web HNS and the scripts use instead of assuming hex.
The Ethereum deploy key is unmistakably separate: ETH_PRIVATE_KEY,
ETH_RPC_URL, ETH_CHAIN_ID, ETH_NETWORK (was WEB3KEY / WEB3PROVIDER /
CHAINID / NETWORK). Env examples and docs updated.

Claude-Session: https://claude.ai/code/session_01WbVfqjbrT8288kHxxZmKuH
Signed-off-by: Roberto Valenti <robbyvalenti@gmail.com>
VITE_HOLOSPHERE_NSEC is gone. The web HNS registry id comes from
VITE_HOLOSPHERE_NPUB (npub1… or hex) via the new npubToHex / toNpub
helpers in @holons/core/nostr; HOLOSPHERE_NSEC stays server-side. The
key generator prints both halves with the right env names, and the
Netlify secrets note now states the rule: no secret ever carries a
VITE_ prefix.

Claude-Session: https://claude.ai/code/session_01WbVfqjbrT8288kHxxZmKuH
Signed-off-by: Roberto Valenti <robbyvalenti@gmail.com>
…ram / discord / tooling)

Sections now follow who reads each variable, with the browser-only
(VITE_) block explicitly public. Adds the keys that were read but never
documented (OPENAI, DISCORD_*, MINIAPP_SHORT_NAME, DASHBOARD_ADDRESS,
VITE_KIOSK_MINIAPP, VITE_VOICE_MODE, VITE_QR_BASE_URL) and fixes the dev
auto-login names to the VITE_DEV_TELEGRAM_USER_* the code reads.

Claude-Session: https://claude.ai/code/session_01WbVfqjbrT8288kHxxZmKuH
Signed-off-by: Roberto Valenti <robbyvalenti@gmail.com>
…screen board when idle

Tab strip
- Drag a tab to reorder it (mouse: on move; touch: hold, then move so a
  swipe still scrolls). Order persists per device (kiosk_tab_order);
  hidden tabs keep their slot (`$lib/taborder`, with tests).
- Hold puts the strip in edit mode: every tab wears a ✕ that hides it
  (never the last one). A "+" sticks to the right edge and lists the
  hidden tabs; picking Status passes through its framing modal, now a
  shared StatusConfirm component used by Settings too.
- Names show while they fit, glyphs alone when they don't, and the strip
  scrolls sideways when even the glyphs overflow; the active tab is kept
  in view. Replaces the fixed 560px icon-only rule.
- Pinning stays on the active tab's pin button.

Settings
- Every tab switch writes through one `setTabShown`, the same path the
  strip's + and ✕ use, so the two surfaces never disagree.
- New Tasks and Calendar switches (on by default) so any tab can go.
- The Shifts sub-line no longer names the Elinor format.

Board
- When the chrome retreats (idle) the card and the page surface close
  their frames and the board runs edge to edge; any touch brings the
  frames back on the header's timing.
- Sky, frame, header padding and corner radii scale with the viewport
  (clamped), so a phone keeps only a sliver of frame.

Dock
- The "+" accepts an id, a registered label, an npub, an Ethereum
  address, a pasted link or a bare name (parseHolonAdd); board → hub
  wording in the dock and settings copy.

Claude-Session: https://claude.ai/code/session_01Xv1XrPrRvcrsUuLeVrH797
Signed-off-by: Roberto Valenti <robbyvalenti@gmail.com>
…he relay-only architecture

- ONBOARDING.md: the relays are the wire; local store per surface; every
  app and package in the repo map; single root .env; branch from `dev`.
- README.md: architecture diagram and packages table cover web, kiosk,
  wequest, telegram, discord, text, ai, mcp, voice and holosphere; quick
  start uses the root env template; docs list links the Nostr docs.
- CONTRIBUTING.md / TUTORIAL.md: root .env, `dev` as the target branch,
  jest + testenv for holosphere changes, rebuild core before running apps.
- docs/nostr-onboarding.md: rewritten for the current state — store
  tables, LWW rule, cursor/backfill behaviour from relay-transport.js,
  projections on by default, nsec key naming, kiosk data path.
- apps/kiosk: README lists the eight tabs, the editable strip and dock,
  the real data path and dev commands; .env.example documents
  VITE_KIOSK_PROJECTIONS; docs/README.md indexes the onboarding.

Claude-Session: https://claude.ai/code/session_01XTZRBNzjaUKXmXskxst6tn
Signed-off-by: Roberto Valenti <robbyvalenti@gmail.com>
… config

The relay is the only wire; these were the dead leftovers still naming Gun:

- holosphere/spike: the four scripts still imported 'gun' (no longer a
  dependency) and wrote to sphere.gun. Rewritten over two HoloSphere
  instances sharing a relay — publish, lose the cache, rehydrate — and the
  README describes the relay-only flow.
- holosphere/examples: the Svelte example called the removed radisk API;
  htmldemo loaded a gun-era CDN build (holosphere@1.1.14). Both now use
  the v2 config / the local rebuilt bundle. Landing copy no longer says
  "Built on GunDB".
- telegram-ui: log strings said "GunDB via HoloSphere 1.3"; the unused
  dbPath getter defaulted to ./radata; radata/** ignore patterns dropped
  from eslint, jest and vitest configs.
- comments in Map/VideoCall and two holosphere tests (waitForGun →
  waitForStore); CLAUDE.md, AGENTS.md and the PR template now warn about
  holosphere-store/ instead of radata/.

Claude-Session: https://claude.ai/code/session_01A4KAj3YieXCgn3ipdRzdpy
Signed-off-by: Roberto Valenti <robbyvalenti@gmail.com>
…l federation

The dock gains an earth under its sky. DockMap projects each board's claimed
hexagon into field pixels (projectBoards), answers what cell a point falls on
(cellAt), and the orbs hover above their places tied down by beacon cones.
Dragging an orb onto a bare hexagon asks to make that cell its home, and
HexPicker now announces a claim so the beacon lands at once rather than on the
next look-up.

FederationLens becomes a flow editor for one pair: two arrows per lens, one
each way, each carrying its source holon's colour, so direction is drawn
rather than spelled. Opening it federates nothing — the record is written when
the first flow opens and removed when the last one closes.

Three fixes to how the dock behaves:

Every circle was named after whichever board was open before it. The layout
wrote the dock entry from two independent stores, and the holon id changes a
tick before the async bind clears the name, so a new board was stamped with
the previous one's. Holons that publish no name of their own then kept that
borrowed name forever, since an empty name was never allowed to clobber a
learned one. The name is now written only where it is resolved, by the code
that knows which holon it resolved for, and an unresolved holon falls back to
its registered label or its bare id (renameEntry/nameBoard). The dock
re-resolves every circle while it is up, which also heals entries already
stored wrong.

Dropping a federated orb on a hexagon always came out a federation. The drop
scan captured any orb within a full diameter of the pointer, but linkOverlap
tows a federated partner along within 1.9 radii of the carried orb for the
whole drag — so the partner matched on every frame and the hexagon only lit
between them. Capture now requires the pointer to be inside the target orb,
one radius, matching what taps and the intersection lens already used
(orbUnder).

Orbs piled onto a single pixel at the edges. The pull toward a place grows
with the distance to it, so a place far outside the view slams its orb into
the boundary with a force no repulsion can answer, and several such places
stacked their orbs into one of which only the top was tappable. Anchors are
clamped into the field first, which makes that pull local and weak enough for
repulsion to spread the parked orbs along the edge. The boundary margin is
also capped at a third of the field, not a half, which collapsed every orb
onto one line on a field narrower than 2.6 radii.

Six specs added for the new pure helpers, each checked to fail without its fix.

Claude-Session: https://claude.ai/code/session_01A4KAj3YieXCgn3ipdRzdpy
Signed-off-by: Roberto Valenti <robbyvalenti@gmail.com>
…license

The agreement no longer carries the "unexecuted template — have it reviewed,
then sign" banner, and README loses the matching "Status & how to use" section
covering signature, counsel review, registration and the assignment upgrade
path. What stays is the chain-of-title table itself.

Also drops the parenthetical "(formerly Harvest)" from the recitals: the
project has one name.

Claude-Session: https://claude.ai/code/session_01A4KAj3YieXCgn3ipdRzdpy
Signed-off-by: Roberto Valenti <robbyvalenti@gmail.com>
Both subdomain labels resolved to -5459621960. They now resolve to
-1003691108237, so commons.hubs.network and valley.hubs.network open the new
holon, as does the /commons and /valley path on any host.

docs/nostr-onboarding.md carried the old id as its running Commons Hub example
throughout — the event sample, the filter examples, the nak and strfry
commands, the vocabulary entry and the subdomain table — so it moves with it.
Test fixtures in @holons/core keep the old string: there it is an arbitrary
group id, not a route.

Claude-Session: https://claude.ai/code/session_01A4KAj3YieXCgn3ipdRzdpy
Signed-off-by: Roberto Valenti <robbyvalenti@gmail.com>
… sync gates

Groundwork for making a lens able to declare a standard Nostr kind as its
canonical wire. Four fixes that stand on their own, none of which changes
observable behaviour while 30078 is the only kind in play.

The envelope index was keyed by pubkey alone, so one author's two encodings of
the same record — the kind-30078 envelope and a standard kind — collided in one
slot even though NIP-33 makes them independent replaceable slots on the relay.
The first standard write by our own key would evict the legacy envelope. The
record survived, which is what made it easy to miss: getEvents, exportEvents,
nextCreatedAt and enforce mode all read envelopes, not records. Keyed by author
and kind now, in both the apply path and the snapshot-hydration path. Missing
the second would have dropped standard events from the index on every restart,
silently, and left nextCreatedAt emitting colliding timestamps.

exportEvents narrowed by reading `h` and `l` straight off the event. Those tags
are the 30078 envelope's way of carrying the address; another encoding carries
it elsewhere, so a narrowed export would have returned nothing at all for such
a lens. It decodes instead, and only when a narrowing argument is given.

The paginated backfill counted "events the store accepted" as pagination
progress. That conflated two unrelated things: an event of a kind this store
does not consume, and an author's superseded claim, which `_storeEvent` drops
so it never enters the events table. Neither is ever reported as seen, so both
read as progress on every page and the walk could only end at the page cap or
the end of history. It now counts ids this run has not walked, so `fresh === 0`
means what the loop needs to know: the page was entirely a re-fetch.

The live subscription advanced the sync cursor on a deny-list of three reasons,
which let every reason nobody had thought of yet advance it and silently skip
that history on the next catch-up. Inverted to an allow-list.

Verified: the holosphere suite passes in all three signing modes, default,
shadow and enforce. The two claim-keying tests fail against the old code.

Claude-Session: https://claude.ai/code/session_01A4KAj3YieXCgn3ipdRzdpy
Signed-off-by: Roberto Valenti <robbyvalenti@gmail.com>
The store took exactly one event kind and read a record's address straight off
the envelope's `h` and `l` tags. Both are now per-kind decisions, held in a
small registry: which kinds this store consumes, and how each one decodes into
an address. That is what a lens needs before it can name a standard Nostr kind
as its canonical encoding instead of the kind-30078 envelope.

A store nobody configured is unchanged. The registry is seeded with the legacy
envelope alone, and a codec cannot claim kind 30078 even if it asks to, so the
envelope grammar can never be hijacked by a lens.

`decode` returns an array rather than one claim, because an event may name
several addresses at once — a NIP-09 kind 5 retracts every coordinate in its
`a` tags. So `apply` now runs the address work per claim, reports `applied`
when at least one landed, and returns them all as `records`. Its `record` stays
the first, which is what every existing caller reads.

The registry is mutable because `enableSigning` can register projections long
after the store was built. A codec's grammar is untrusted input, so a decoder
that throws makes the event malformed rather than taking the store down.

Snapshot hydration decodes through the same registry as `apply`. Reading the
envelope tags directly there would have dropped every standard-kind event from
the claim index on restart, silently, with the records still present and
`nextCreatedAt` handing out timestamps that collide on the relay.

Nothing declares a standard wire yet; this is the mechanism only. The relay
filter and the signing layer's envelope decode are still envelope-shaped and
come next.

Verified in all three signing modes: default, shadow and enforce.

Claude-Session: https://claude.ai/code/session_01A4KAj3YieXCgn3ipdRzdpy
Signed-off-by: Roberto Valenti <robbyvalenti@gmail.com>
Completes the plumbing a standard-primary lens needs. The store could already
consume more than one kind; now the relay layer can fetch them and the signing
layer can read them.

`filterFor` becomes `filtersFor`, returning one REQ shape per wire. The legacy
envelope is always first and always carries the wire id '30078', which is what
keeps every cursor already on disk resolving to the same key. A lens whose
codec claims a standard kind contributes its own shapes after it, and each gets
its own subscription, its own reopen backoff and its own cursor.

Separate cursors are the point, not an implementation detail. Two wires on one
lens have unrelated timestamp distributions: a lens frozen on the envelope
while its standard kind advances would have a shared cursor dragged past the
envelope's own tail, silently skipping a straggler from a peer still running
old code. It also means a lens long warm on the envelope BACKFILLS a wire it
has never carried, rather than catching up from a `since` that belongs to a
different stream — which is how a peer's older history gets found at all. The
new suite covers exactly that and fails against a shared cursor.

The signing layer decoded envelopes with `eventToItem`, which reads the event's
content as the item. That is only true of the envelope. A lens carried on a
standard kind keeps its fields elsewhere, so every item on such a lens would
have read as unaccounted: pending under enforce, a mismatch under shadow. Four
call sites now decode through the store's wire and ask what an event claims AT
one address, which also handles an event claiming several. The fifth is left
alone on purpose and says so: it only ever folds the internal membership log,
which can never be carried on a standard kind.

A HoloSphere can now be handed a wire registry as `store.wire`. Omitted, it
gets the envelope alone, so nothing that exists today changes.

Verified in enforce mode: 54 suites, 290 passing.

Claude-Session: https://claude.ai/code/session_01A4KAj3YieXCgn3ipdRzdpy
Signed-off-by: Roberto Valenti <robbyvalenti@gmail.com>
The pilot for standard-primary lenses, and the first one that pays off: a
Holons surface can now read Elinor's schedule with an ordinary lens call.
Verified against the live relay — `getAll('-1003691108237', 'shifts')` returns
105 occurrences and 56 signups, with holders resolved per occurrence.

Nothing is asked of Elinor. These are the NIP-52 events it already publishes;
this decodes them into records at ordinary lens addresses. An occurrence lands
on `shifts` at `<date>-<code>`, and each signup lands on `shifts_rsvp` under
its own address rather than being merged into the occurrence, which would
re-enter the write path and the projection ratchet reverse sync exists to
avoid.

Read-only on the wire, deliberately. Occurrences are the coordinator's to
publish, and a signup carries a person-level rule — newest across a person's
linked keys — that the generic per-address write path would get wrong. So there
is no encoder, and signups keep going out through `buildRsvpTemplate`.

A signup's occurrence is read from its `a` address rather than its own `d` tag.
That is not defensive coding: Elinor appends the subject's key prefix to an
on-behalf-of request, a grammar the shift d-tag rule does not admit, so reading
the d tag would drop those events entirely. Real fixtures pulled off the relay
cover it.

Also honours the marker divergence found earlier. Elinor treats a `p` tag
marked `on-behalf-of` as a request an agent published for someone, not
occupancy, because the real signup follows under the member's own key. These
records are marked and never counted, so a request no longer surfaces as a
phantom participant attributed to the agent.

Authorization matters more here than on the envelope: a lens carried on a
standard kind is permissionless BY KIND, and anyone can publish a 31923 with a
group tag. Occurrences are the coordinator's alone when one is pinned, and a
signup's signer is always its own subject.

Claude-Session: https://claude.ai/code/session_01A4KAj3YieXCgn3ipdRzdpy
Signed-off-by: Roberto Valenti <robbyvalenti@gmail.com>
… surfaces

mcp-ui: federation_link_add / federation_link_remove return the
{ self, partner } outcome with a plain note; new federation_requests
(pending requests, optional per-lens link state with a partner) and
federation_request_answer (admin-gated accept / decline).

discord-ui and wequest say when the partner's admins were asked, and
when this key may not change the holon's federation.

Signed-off-by: Roberto Valenti <robbyvalenti@gmail.com>
Each taskchat message is published as a kind-9 group message whose h tag is a stable per-task group id derived from holon and task key.

Signed-off-by: Roberto Valenti <robbyvalenti@gmail.com>
taskRef resolves holograms and federated copies to the source task so everyone shares one chat and one board. Chat messages live in the source holon's taskchat lens (members post, private tasks have none); boardUrl is an excalidraw.com live room minted once and validated strictly. Quest.canvasId is retired.

Signed-off-by: Roberto Valenti <robbyvalenti@gmail.com>
Signed-off-by: Roberto Valenti <robbyvalenti@gmail.com>
Signed-off-by: Roberto Valenti <robbyvalenti@gmail.com>
The Kudos track counted quest:initiated and quest:completed as kudos, so
creating a task showed up as kudos received from no one (ledger) or given
to the holon (chord). Kudos are now only appreciation:sent — one person
thanking another. The ledger events are untouched; scoring still reads them.

Signed-off-by: Roberto Valenti <robbyvalenti@gmail.com>
The people pickers (edit form hosts/participants, the card's Add someone,
the completion dialog) still list the holon's members at once; a last
entry, "Search the federation…", opens an inline search over the people
of the holon's direct federation partners.

Core: @holons/core/holons loadFederationPeople reads the partners' users
rosters folded with the holon's own (linked keys recognised, members left
out) and findPeople matches by name, @username or id.

Signed-off-by: Roberto Valenti <robbyvalenti@gmail.com>
…hows them

A Telegram file_id opens only with the token of the bot that received it, so
photos sent to a group run by another bot (Lunation 83/84) showed nowhere.

- core `@holons/core/media`: uploadBlob (BUD-02 PUT + BUD-11 kind-24242 auth
  scoped to blob and server), resolveMediaServers, and pictureUrl as the one
  rendering rule (replaces kiosk lib/image.ts and web utils/imageServer.ts;
  raw base64 is now classified before paths)
- bot: hostTelegramPicture stores a hosted URL for quests, events, expenses
  and tags, keeping the file_id when the upload fails
- web: task pictures upload with the member's key, data URI as fallback
- nostr: only http(s) pictures go in `image` tags
- BOT_TOKENS lets /api/image open old file_ids from the other bots
- deploy/blossom: docker-compose + config for blossom.holons.io
- scripts/rehost-pictures.mjs: copy old file_id pictures (dry-run default)

Signed-off-by: Roberto Valenti <robbyvalenti@gmail.com>
… equation

A mutual-credit entry paid into the holon itself (splitWith = [holonId])
is a contribution: it adds to the giver's score in full and takes nothing
from the other members. Money already worked that way; hours did not —
the scorer read hours only from task time logs and skipped the hour
balance, so hours declared in mutual credit scored nothing.

A member's hours are now the task-logged hours plus the hour balance,
less the hour expenses a task completion writes (tagged fromTimeTracking
in the REA context, older ones recognised by their <task>_time_<user>
id), so completed-task hours still count once. REAAggregator's
calculateUserScore goes through the same rule and no longer counts those
hours twice.

Signed-off-by: Roberto Valenti <robbyvalenti@gmail.com>
An "Into {holon}" choice beside the people an expense is split with
records it as given to the holon: it counts toward the giver's share of
the fund and nobody owes it back. Such records read "{name} put into
{holon}" and "Counts toward your share" for the giver.

Signed-off-by: Roberto Valenti <robbyvalenti@gmail.com>
The length buttons used lb_n_<minutes>, which the scheduler's unanchored /n_(.+)/ answered first, so picking a length did nothing. They are lb_l_<minutes> now, and a test checks that no other module's action pattern matches a booking button.

Signed-off-by: Roberto Valenti <robbyvalenti@gmail.com>
…rote

A removal dated before the key spoke falls back to the earlier word; one dated after keeps it; a removed key that writes again replaces its own accepted claim and the record falls back past it.

Signed-off-by: Roberto Valenti <robbyvalenti@gmail.com>
…rafts where you look

Android fires contextmenu on a touch long-press and the menu it pops
cancels the pointer underneath, so every hold gesture on the kiosk died
at ~500 ms: lifting a drawer chip onto a day never scheduled the task,
holding an empty slot never drew an event, holding a note never
reordered it. The layout now swallows the menu for touch/pen presses
outside text fields (holdmenu.ts decides; a mouse right-click keeps it).

The calendar's + always drafted the event at "now", so pressed over
another day it wrote into today, out of sight and not in the drawer
(dated). It now drafts in the window on screen — today while shown,
else the window's first day at the present time — and is labelled
"New event": drawn on the calendar means an event, the Tasks wall's +
stays a task.

Signed-off-by: Roberto Valenti <robbyvalenti@gmail.com>
telegram.holons.io resolves a file_id with this bot's token first, then each token in BOT_TOKENS, so file_ids from groups run by another bot (@HubsNetwork_bot) open too. The web and kiosk /api/image fall back to its /getfile after their own tokens and the peer proxy.

Signed-off-by: Roberto Valenti <robbyvalenti@gmail.com>
iOS Safari ignores the viewport meta's user-scalable=no and a trackpad
pinch arrives as ctrl+wheel. lib/nozoom.ts cancels Safari's gesture
events, multi-finger touchmove and ctrl+wheel; html gets touch-action
pan-x pan-y. The timeline and task graph keep their own content zoom.

Signed-off-by: Roberto Valenti <robbyvalenti@gmail.com>
The year line takes an optional range [from, to) and title; the calendar
adds a second line spanning new moon to new moon (core lunationAt), with
its moons pinned to the edges and month/solar markers clipped to the span.

Signed-off-by: Roberto Valenti <robbyvalenti@gmail.com>
… deep link

New ikigai and linkedin scenes (in the default and /onboarding sequences
and the welcome Change menu). ?start=onboarding runs the whole journey for
new people and only the public-profile steps for people the bot knows.

Signed-off-by: Roberto Valenti <robbyvalenti@gmail.com>
…it works

The welcome text no longer repeats the board URL that its button already
carries. A new button row opens the dashboard's settings tab and posts the
/help guide in the chat, on /start, /id, group joins and chat links.

Signed-off-by: Roberto Valenti <robbyvalenti@gmail.com>
…s API

Location is no longer asked or stored (scene, both sequences, Change menu,
DNA field). The onboarding Q&A, the question picker and /summarize move off
the shut-down Assistants API onto the Responses API: the conversation
continues by previous_response_id, the agent answers from the /help guide,
OPENAI_MODEL overrides gpt-4o, and the question picker falls back to the
questionnaire order when the AI fails. ontology() had no callers and is gone.

Signed-off-by: Roberto Valenti <robbyvalenti@gmail.com>
…alogue

The picker reads data/values.json (125 values) instead of eight hard-coded
ones: ten per page in two columns with a page counter, and each value's
meaning listed in the message.

Signed-off-by: Roberto Valenti <robbyvalenti@gmail.com>
…for speech only

@holons/core/ai holds the client, model (HOLONS_AI_MODEL, default
claude-opus-5-5), effort, structured JSON output, the refusal fallback and
one AIError with an HTTP status and a public message.

- web/kiosk breakdown and kiosk drafting use structured output
  (BREAKDOWN_SCHEMA, buildDraftSchema) server-side only; the browser
  OpenAI path is gone
- the bot's onboarding agent, question picker, /summarize and photo
  reading run on Claude; unused OpenAI helpers deleted
- ai-ui AnthropicProvider and voice-ui go through core complete()
- speech (kiosk voice relay, voice-ui STT/TTS) reads OPENAI_API_KEY only;
  every VITE_*_API_KEY is removed
- holosphere's dead OpenAI summarize path removed
- @anthropic-ai/sdk 1.1, kept SSR-external in kiosk and web

Signed-off-by: Roberto Valenti <robbyvalenti@gmail.com>
Both manifests (static + per-hub) locked the installed app to portrait;
orientation is now "any" so a wall-mounted tablet opens in landscape.

Signed-off-by: Roberto Valenti <robbyvalenti@gmail.com>
The service worker fell back to /index.html offline, which never exists
(ssr off, nothing prerendered) — an offline launch got a bare 503. It
now fetches and caches the app shell at install and serves it to
navigations when the network is gone.

Manifest: display_override (standalone where fullscreen isn't offered),
lang, categories.

Signed-off-by: Roberto Valenti <robbyvalenti@gmail.com>
Orbs shrink from 6.5rem to 4.75rem. Picking a hub that is already
docked opens its board instead of flying the earth to it.

Signed-off-by: Roberto Valenti <robbyvalenti@gmail.com>
A hub's ground was as wide as its farthest member, so one member drawn
off by a second hub widened the whole ground, pushed everyone else out,
stretched their hubs in turn — 60 people settled ~400 radii apart. The
ground (and the hub-apart distance) is now capped at what the members
need packed: 60 people settle in ~27 radii, 150 in ~44, stable live.

The view may shrink to 0.2 (was 0.4): a full dock on a phone needs ~0.25,
and small orbs beat people off screen.

Signed-off-by: Roberto Valenti <robbyvalenti@gmail.com>
Every edit form writes as you go: a burst of edits is one put after a
short quiet spell, closing the form (Done, ✕, backdrop, Esc, another
card) writes whatever is still waiting, and writes go out in order.
One helper ($lib/autosave) with a spec; the state is a pill in the
sheet's corner beside ✕ (SaveState) — "Saving…", "Saved", or a failed
write that is itself the retry button.

Covered: task/event and library item (DetailModal), role, stock item,
offer, shift plan, and the Flows split. Kept explicit: creating a new
item (it needs its title first), the Flows rules (a signed log entry),
and the Pot panel (on chain).

The task form is regrouped into what / when / who / where; kind and
cadence collapse to one line until there is a date; the edit switch no
longer inherits the view's uppercase `.kind` badge. Library item:
description first, category and value side by side. Shift plan copy
no longer asks for a Save that is gone.

Signed-off-by: Roberto Valenti <robbyvalenti@gmail.com>
parseWebApplication validates the record liminalvillage.com/apply sends
(64 KB cap, unknown keys dropped, list/text types per data key).
storeApplication keeps it in the holon's applications lens only after
ensureApplicationsSealed has locked the lens (audience: the holon) and
holds its key; otherwise it throws ApplicationsNotSealedError and writes
nothing. A repeated id keeps the stored record.

Signed-off-by: Roberto Valenti <robbyvalenti@gmail.com>
booking.json now asks the liminalvillage.com/apply questions in its order:
role is a multi choice, a visit question gates the travel questions,
transport and accommodation are optional, referral/x/linkedin/website are
new, and social links are asked only when x, LinkedIn and website are all
empty. BookingSystem honours a declarative showIf (in, notIn, allEmpty, or
a list of them) wherever it advances; skipped fields are neither asked nor
stored. Select answers are stored as option values (string or list, with
'other') and Other's text in <key>_other, instead of { value, customValue }
objects and 'other:<text>' entries.

Signed-off-by: Roberto Valenti <robbyvalenti@gmail.com>
Bearer APPLICATIONS_TOKEN (constant-time; unset refuses everything),
body validated by @holons/core/applications, stored in
APPLICATIONS_HOLON (default Liminal Village) lens applications under the
web id. 503 rather than a plaintext write when the lens cannot be sealed.

Signed-off-by: Roberto Valenti <robbyvalenti@gmail.com>
Applicants can let their ikigai and LinkedIn go on their public profile
(public_profile: yes). Web applicants claim it with a t.me/HolonsBot
?start=profile_<claim> link, which writes just those two answers to the
DNA record in their own holon; /apply in Telegram does it directly.

Signed-off-by: Roberto Valenti <robbyvalenti@gmail.com>
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