@nelsonlongxiang/dsh-open-a2a-net 0.6.38 → 0.6.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +340 -335
  2. package/README_zh.md +241 -236
  3. package/assets/nexusDist/assets/index-C7HagaGy.css +1 -1
  4. package/assets/nexusDist/assets/index-NkNSeIx6.js +9 -9
  5. package/cli/a2a.mjs +12 -4
  6. package/cli/serve.mjs +132 -0
  7. package/cordis.patch.yml +21 -21
  8. package/lib/canvas-id.js +27 -0
  9. package/lib/canvas-store.js +7 -1
  10. package/lib/client.js +63 -63
  11. package/lib/config.js +125 -0
  12. package/lib/direct-forward.js +64 -18
  13. package/lib/faces/admin.js +4 -1
  14. package/lib/faces/wire.js +24 -6
  15. package/lib/flood-guard.js +3 -1
  16. package/lib/index.js +43 -145
  17. package/lib/live-roots.js +39 -0
  18. package/lib/origin-claim.js +3 -2
  19. package/lib/queue-guard.js +3 -1
  20. package/lib/skill/references/cli.md +214 -203
  21. package/lib/skill/references/routing.md +4 -0
  22. package/lib/task-auth.js +14 -10
  23. package/lib/types/canvas-id.d.ts +9 -0
  24. package/lib/types/config.d.ts +278 -0
  25. package/lib/types/direct-forward.d.ts +25 -3
  26. package/lib/types/faces/admin.d.ts +2 -0
  27. package/lib/types/faces/wire.d.ts +7 -0
  28. package/lib/types/index.d.ts +3 -277
  29. package/lib/types/live-roots.d.ts +35 -0
  30. package/lib/types/origin-claim.d.ts +1 -0
  31. package/lib/types/queue-guard.d.ts +2 -0
  32. package/lib/types/task-auth.d.ts +3 -0
  33. package/lib/types/types.d.ts +2 -0
  34. package/nexus-stage/package-lock.json +2262 -2262
  35. package/nexus-stage/scripts/__pycache__/mockhost.cpython-312.pyc +0 -0
  36. package/nexus-stage/src/edges.ts +72 -72
  37. package/nexus-stage/src/layout-wire.ts +158 -158
  38. package/nexus-stage/src/seat.ts +35 -35
  39. package/nexus-stage/src/stage-name.ts +13 -13
  40. package/nexus-stage/src/tokens.ts +31 -31
  41. package/package.json +218 -218
@@ -1,72 +1,72 @@
1
- /**
2
- * Star-shaped membership edges for the 2D planning canvas: one spoke per
3
- * (team frame -> member card), replacing the O(n^2) pairwise mesh. All
4
- * geometry is world-space so the SVG layer can emit raw coordinates - the
5
- * transformed #world container carries them to the screen for free.
6
- *
7
- * Anchor contract (must stay in lockstep with planning.css):
8
- * - origin = the frame titlebar's bottom-edge midpoint. The head is a
9
- * 24px border-box straddling the frame's top border at top:-13px, so
10
- * its bottom edge sits at fy + 11, horizontally centered at fx + fw/2.
11
- * - target = the member card's top-edge midpoint {nx, ny - NODE_H/2}.
12
- *
13
- * Cross-team members draw solid from their first membership (state order)
14
- * and dashed from every later one - the many-to-many invariant made
15
- * visible. Edges are derived only; there is no freehand edge in this
16
- * canvas. Pure: no DOM, no three.
17
- * @module nexus-stage/edges
18
- */
19
-
20
- import { NODE_H } from './world'
21
- import type { FrameGeom } from './world'
22
-
23
- /** Titlebar border-box height (planning.css .frame-head must match). */
24
- export const FRAME_HEAD_H = 24
25
- /** Titlebar CSS top offset, negative - straddles the frame's top border. */
26
- export const FRAME_HEAD_TOP = -13
27
- /** Origin anchor: frame top + the head's bottom edge offset. */
28
- export const FRAME_HEAD_LIFT = FRAME_HEAD_TOP + FRAME_HEAD_H
29
-
30
- /** Minimal team shape (topology.CanvasTeam satisfies it). */
31
- export interface EdgeTeam { name: string; members: ReadonlyArray<{ id: string }> }
32
-
33
- /** One derived spoke, ready for the SVG layer. */
34
- export interface StarEdge {
35
- x1: number
36
- y1: number
37
- x2: number
38
- y2: number
39
- /** True from a member's second membership on (cross-team many-to-many). */
40
- dashed: boolean
41
- /** Team name and member id, for keyed diffing. */
42
- team: string
43
- id: string
44
- }
45
-
46
- /**
47
- * Compute every membership spoke for the current world. Frames without a
48
- * rect and member ids without a position are skipped - dangling rows are
49
- * a polling transient, not an error.
50
- */
51
- export function starEdges(
52
- teams: ReadonlyArray<EdgeTeam>,
53
- frames: ReadonlyMap<string, FrameGeom>,
54
- positions: ReadonlyMap<string, { readonly x: number; readonly y: number }>,
55
- ): StarEdge[] {
56
- const out: StarEdge[] = []
57
- const seen = new Set<string>()
58
- for (const team of teams) {
59
- const rect = frames.get(team.name)
60
- if (rect === undefined) continue
61
- const x1 = rect.x + rect.w / 2
62
- const y1 = rect.y + FRAME_HEAD_LIFT
63
- for (const member of team.members) {
64
- const pos = positions.get(member.id)
65
- if (pos === undefined) continue
66
- const dashed = seen.has(member.id)
67
- seen.add(member.id)
68
- out.push({ x1, y1, x2: pos.x, y2: pos.y - NODE_H / 2, dashed, team: team.name, id: member.id })
69
- }
70
- }
71
- return out
72
- }
1
+ /**
2
+ * Star-shaped membership edges for the 2D planning canvas: one spoke per
3
+ * (team frame -> member card), replacing the O(n^2) pairwise mesh. All
4
+ * geometry is world-space so the SVG layer can emit raw coordinates - the
5
+ * transformed #world container carries them to the screen for free.
6
+ *
7
+ * Anchor contract (must stay in lockstep with planning.css):
8
+ * - origin = the frame titlebar's bottom-edge midpoint. The head is a
9
+ * 24px border-box straddling the frame's top border at top:-13px, so
10
+ * its bottom edge sits at fy + 11, horizontally centered at fx + fw/2.
11
+ * - target = the member card's top-edge midpoint {nx, ny - NODE_H/2}.
12
+ *
13
+ * Cross-team members draw solid from their first membership (state order)
14
+ * and dashed from every later one - the many-to-many invariant made
15
+ * visible. Edges are derived only; there is no freehand edge in this
16
+ * canvas. Pure: no DOM, no three.
17
+ * @module nexus-stage/edges
18
+ */
19
+
20
+ import { NODE_H } from './world'
21
+ import type { FrameGeom } from './world'
22
+
23
+ /** Titlebar border-box height (planning.css .frame-head must match). */
24
+ export const FRAME_HEAD_H = 24
25
+ /** Titlebar CSS top offset, negative - straddles the frame's top border. */
26
+ export const FRAME_HEAD_TOP = -13
27
+ /** Origin anchor: frame top + the head's bottom edge offset. */
28
+ export const FRAME_HEAD_LIFT = FRAME_HEAD_TOP + FRAME_HEAD_H
29
+
30
+ /** Minimal team shape (topology.CanvasTeam satisfies it). */
31
+ export interface EdgeTeam { name: string; members: ReadonlyArray<{ id: string }> }
32
+
33
+ /** One derived spoke, ready for the SVG layer. */
34
+ export interface StarEdge {
35
+ x1: number
36
+ y1: number
37
+ x2: number
38
+ y2: number
39
+ /** True from a member's second membership on (cross-team many-to-many). */
40
+ dashed: boolean
41
+ /** Team name and member id, for keyed diffing. */
42
+ team: string
43
+ id: string
44
+ }
45
+
46
+ /**
47
+ * Compute every membership spoke for the current world. Frames without a
48
+ * rect and member ids without a position are skipped - dangling rows are
49
+ * a polling transient, not an error.
50
+ */
51
+ export function starEdges(
52
+ teams: ReadonlyArray<EdgeTeam>,
53
+ frames: ReadonlyMap<string, FrameGeom>,
54
+ positions: ReadonlyMap<string, { readonly x: number; readonly y: number }>,
55
+ ): StarEdge[] {
56
+ const out: StarEdge[] = []
57
+ const seen = new Set<string>()
58
+ for (const team of teams) {
59
+ const rect = frames.get(team.name)
60
+ if (rect === undefined) continue
61
+ const x1 = rect.x + rect.w / 2
62
+ const y1 = rect.y + FRAME_HEAD_LIFT
63
+ for (const member of team.members) {
64
+ const pos = positions.get(member.id)
65
+ if (pos === undefined) continue
66
+ const dashed = seen.has(member.id)
67
+ seen.add(member.id)
68
+ out.push({ x1, y1, x2: pos.x, y2: pos.y - NODE_H / 2, dashed, team: team.name, id: member.id })
69
+ }
70
+ }
71
+ return out
72
+ }
@@ -1,158 +1,158 @@
1
- /**
2
- * The planning canvas's layout save loop: debounce dirty flags into one
3
- * POST, drive the save lamp (idle -> pending -> saved | error), and adopt
4
- * the host's normalized snapshot without ever fighting an in-flight drag.
5
- *
6
- * Discipline (born of the review's "empty catch" incidents):
7
- * - the payload is read at SEND time, so edits during the debounce window
8
- * ride the same request;
9
- * - on success the sent document is compared against the live one - if
10
- * they differ, the user edited during flight: positions are NOT touched
11
- * (the live model is authoritative), the lamp stays pending, and the
12
- * loop chains the follow-up save. Only an unchanged document adopts the
13
- * host's normalized response, and because the client pre-clamps with
14
- * the host-mirror discipline, our own echo is byte-equal and adoption
15
- * is a no-op in the common case;
16
- * - failures surface (lamp error + the stage's throttled fault reporter),
17
- * never swallowed - retry is explicit (click) or implicit (next edit).
18
- *
19
- * Pure control flow: the transport, the timer, and the clock are all
20
- * injected, so tests pin the ladder without a network or real time.
21
- * @module nexus-stage/layout-wire
22
- */
23
-
24
- /** Save-lamp states: grey=start, amber=unsaved, green=stored, red=failed. */
25
- export type LampState = 'idle' | 'pending' | 'saved' | 'error'
26
-
27
- /** Debounce window collapsing nudge storms into one request. */
28
- export const SAVE_DEBOUNCE_MS = 800
29
-
30
- /** What send() must resolve to: the host's {ok, layout?, error?} body. */
31
- export interface SaveResponse { ok: boolean; layout?: unknown; error?: string }
32
-
33
- export interface SaveLoopDeps {
34
- /** The current layout document, read at send time. */
35
- snapshot(): unknown
36
- /** POST the document; resolve with the parsed host body, throw on transport failure. */
37
- send(doc: unknown, opts: { keepalive: boolean }): Promise<SaveResponse>
38
- /** Timer injection: schedule(fn, ms) returns its cancel function. */
39
- schedule(fn: () => void, ms: number): () => void
40
- /** Injectable clock (reserved for throttle semantics parity with fault.ts). */
41
- now(): number
42
- /** Lamp transitions; never called with the same state twice in a row. */
43
- onLamp(state: LampState): void
44
- /** Adopt the host's normalized layout (only on unchanged-document success). */
45
- onAdopt(layout: unknown): void
46
- /** Transport/refusal failures, for the stage's fault surface. */
47
- onHttpError(message: string): void
48
- }
49
-
50
- export interface SaveLoop {
51
- /** Something moved: lamp pending, debounced save (re-)armed. */
52
- markDirty(): void
53
- /** Explicit retry after an error (lamp click). */
54
- retry(): void
55
- /** Fire now with keepalive - the pagehide path. */
56
- flush(): void
57
- /** True while a request is in flight. */
58
- isBusy(): boolean
59
- /** Cancel the armed timer; the loop stops. */
60
- dispose(): void
61
- }
62
-
63
- /** Create the save loop over the given dependencies. */
64
- export function createSaveLoop(deps: SaveLoopDeps): SaveLoop {
65
- let cancelTimer: (() => void) | undefined
66
- let busy = false
67
- let dirty = false
68
- let flushAfterSettle = false
69
- let lamp: LampState = 'idle'
70
-
71
- function setLamp(next: LampState): void {
72
- if (next === lamp) return
73
- lamp = next
74
- deps.onLamp(next)
75
- }
76
-
77
- function clearTimer(): void {
78
- if (cancelTimer !== undefined) { cancelTimer(); cancelTimer = undefined }
79
- }
80
-
81
- function arm(): void {
82
- clearTimer()
83
- cancelTimer = deps.schedule(() => { cancelTimer = undefined; void fire(false) }, SAVE_DEBOUNCE_MS)
84
- }
85
-
86
- async function fire(keepalive: boolean): Promise<void> {
87
- if (busy) { dirty = true; return }
88
- busy = true
89
- clearTimer()
90
- const sent = deps.snapshot()
91
- let sentJson: string
92
- try {
93
- sentJson = JSON.stringify(sent)
94
- } catch {
95
- busy = false
96
- setLamp('error')
97
- deps.onHttpError('layout document is not JSON-serializable')
98
- return
99
- }
100
- try {
101
- const res = await deps.send(sent, { keepalive })
102
- if (res === null || typeof res !== 'object' || res.ok !== true) {
103
- setLamp('error')
104
- const reason = res !== null && typeof res === 'object' && typeof res.error === 'string' ? res.error : 'layout save refused'
105
- deps.onHttpError(`layout save: ${reason}`)
106
- } else if (JSON.stringify(deps.snapshot()) === sentJson) {
107
- deps.onAdopt(res.layout)
108
- setLamp('saved')
109
- } else {
110
- // Edited mid-flight: the live model stays authoritative; the dirty
111
- // handling below chains the follow-up save.
112
- }
113
- } catch (error) {
114
- setLamp('error')
115
- deps.onHttpError(`layout save unreachable: ${String((error as Error | undefined)?.message ?? error).slice(0, 80)}`)
116
- }
117
- busy = false
118
- if (lamp === 'error') {
119
- // The flush request was consumed by the failed save: retry is
120
- // explicit — do not let the stale flag chain an extra keepalive.
121
- flushAfterSettle = false
122
- return
123
- }
124
- // pagehide raced a busy save: the queued edits must not wait for a
125
- // debounce that a closing page will never fire — flush immediately.
126
- if (flushAfterSettle) {
127
- flushAfterSettle = false
128
- void fire(true)
129
- return
130
- }
131
- if (dirty) {
132
- dirty = false
133
- setLamp('pending')
134
- arm()
135
- }
136
- }
137
-
138
- return {
139
- markDirty() {
140
- dirty = false
141
- setLamp('pending')
142
- if (busy) { dirty = true; return }
143
- arm()
144
- },
145
- retry() {
146
- if (lamp !== 'error' || busy) return
147
- setLamp('pending')
148
- void fire(false)
149
- },
150
- flush() {
151
- if (busy) { flushAfterSettle = true; dirty = true; return }
152
- clearTimer()
153
- void fire(true)
154
- },
155
- isBusy() { return busy },
156
- dispose() { clearTimer() },
157
- }
158
- }
1
+ /**
2
+ * The planning canvas's layout save loop: debounce dirty flags into one
3
+ * POST, drive the save lamp (idle -> pending -> saved | error), and adopt
4
+ * the host's normalized snapshot without ever fighting an in-flight drag.
5
+ *
6
+ * Discipline (born of the review's "empty catch" incidents):
7
+ * - the payload is read at SEND time, so edits during the debounce window
8
+ * ride the same request;
9
+ * - on success the sent document is compared against the live one - if
10
+ * they differ, the user edited during flight: positions are NOT touched
11
+ * (the live model is authoritative), the lamp stays pending, and the
12
+ * loop chains the follow-up save. Only an unchanged document adopts the
13
+ * host's normalized response, and because the client pre-clamps with
14
+ * the host-mirror discipline, our own echo is byte-equal and adoption
15
+ * is a no-op in the common case;
16
+ * - failures surface (lamp error + the stage's throttled fault reporter),
17
+ * never swallowed - retry is explicit (click) or implicit (next edit).
18
+ *
19
+ * Pure control flow: the transport, the timer, and the clock are all
20
+ * injected, so tests pin the ladder without a network or real time.
21
+ * @module nexus-stage/layout-wire
22
+ */
23
+
24
+ /** Save-lamp states: grey=start, amber=unsaved, green=stored, red=failed. */
25
+ export type LampState = 'idle' | 'pending' | 'saved' | 'error'
26
+
27
+ /** Debounce window collapsing nudge storms into one request. */
28
+ export const SAVE_DEBOUNCE_MS = 800
29
+
30
+ /** What send() must resolve to: the host's {ok, layout?, error?} body. */
31
+ export interface SaveResponse { ok: boolean; layout?: unknown; error?: string }
32
+
33
+ export interface SaveLoopDeps {
34
+ /** The current layout document, read at send time. */
35
+ snapshot(): unknown
36
+ /** POST the document; resolve with the parsed host body, throw on transport failure. */
37
+ send(doc: unknown, opts: { keepalive: boolean }): Promise<SaveResponse>
38
+ /** Timer injection: schedule(fn, ms) returns its cancel function. */
39
+ schedule(fn: () => void, ms: number): () => void
40
+ /** Injectable clock (reserved for throttle semantics parity with fault.ts). */
41
+ now(): number
42
+ /** Lamp transitions; never called with the same state twice in a row. */
43
+ onLamp(state: LampState): void
44
+ /** Adopt the host's normalized layout (only on unchanged-document success). */
45
+ onAdopt(layout: unknown): void
46
+ /** Transport/refusal failures, for the stage's fault surface. */
47
+ onHttpError(message: string): void
48
+ }
49
+
50
+ export interface SaveLoop {
51
+ /** Something moved: lamp pending, debounced save (re-)armed. */
52
+ markDirty(): void
53
+ /** Explicit retry after an error (lamp click). */
54
+ retry(): void
55
+ /** Fire now with keepalive - the pagehide path. */
56
+ flush(): void
57
+ /** True while a request is in flight. */
58
+ isBusy(): boolean
59
+ /** Cancel the armed timer; the loop stops. */
60
+ dispose(): void
61
+ }
62
+
63
+ /** Create the save loop over the given dependencies. */
64
+ export function createSaveLoop(deps: SaveLoopDeps): SaveLoop {
65
+ let cancelTimer: (() => void) | undefined
66
+ let busy = false
67
+ let dirty = false
68
+ let flushAfterSettle = false
69
+ let lamp: LampState = 'idle'
70
+
71
+ function setLamp(next: LampState): void {
72
+ if (next === lamp) return
73
+ lamp = next
74
+ deps.onLamp(next)
75
+ }
76
+
77
+ function clearTimer(): void {
78
+ if (cancelTimer !== undefined) { cancelTimer(); cancelTimer = undefined }
79
+ }
80
+
81
+ function arm(): void {
82
+ clearTimer()
83
+ cancelTimer = deps.schedule(() => { cancelTimer = undefined; void fire(false) }, SAVE_DEBOUNCE_MS)
84
+ }
85
+
86
+ async function fire(keepalive: boolean): Promise<void> {
87
+ if (busy) { dirty = true; return }
88
+ busy = true
89
+ clearTimer()
90
+ const sent = deps.snapshot()
91
+ let sentJson: string
92
+ try {
93
+ sentJson = JSON.stringify(sent)
94
+ } catch {
95
+ busy = false
96
+ setLamp('error')
97
+ deps.onHttpError('layout document is not JSON-serializable')
98
+ return
99
+ }
100
+ try {
101
+ const res = await deps.send(sent, { keepalive })
102
+ if (res === null || typeof res !== 'object' || res.ok !== true) {
103
+ setLamp('error')
104
+ const reason = res !== null && typeof res === 'object' && typeof res.error === 'string' ? res.error : 'layout save refused'
105
+ deps.onHttpError(`layout save: ${reason}`)
106
+ } else if (JSON.stringify(deps.snapshot()) === sentJson) {
107
+ deps.onAdopt(res.layout)
108
+ setLamp('saved')
109
+ } else {
110
+ // Edited mid-flight: the live model stays authoritative; the dirty
111
+ // handling below chains the follow-up save.
112
+ }
113
+ } catch (error) {
114
+ setLamp('error')
115
+ deps.onHttpError(`layout save unreachable: ${String((error as Error | undefined)?.message ?? error).slice(0, 80)}`)
116
+ }
117
+ busy = false
118
+ if (lamp === 'error') {
119
+ // The flush request was consumed by the failed save: retry is
120
+ // explicit — do not let the stale flag chain an extra keepalive.
121
+ flushAfterSettle = false
122
+ return
123
+ }
124
+ // pagehide raced a busy save: the queued edits must not wait for a
125
+ // debounce that a closing page will never fire — flush immediately.
126
+ if (flushAfterSettle) {
127
+ flushAfterSettle = false
128
+ void fire(true)
129
+ return
130
+ }
131
+ if (dirty) {
132
+ dirty = false
133
+ setLamp('pending')
134
+ arm()
135
+ }
136
+ }
137
+
138
+ return {
139
+ markDirty() {
140
+ dirty = false
141
+ setLamp('pending')
142
+ if (busy) { dirty = true; return }
143
+ arm()
144
+ },
145
+ retry() {
146
+ if (lamp !== 'error' || busy) return
147
+ setLamp('pending')
148
+ void fire(false)
149
+ },
150
+ flush() {
151
+ if (busy) { flushAfterSettle = true; dirty = true; return }
152
+ clearTimer()
153
+ void fire(true)
154
+ },
155
+ isBusy() { return busy },
156
+ dispose() { clearTimer() },
157
+ }
158
+ }
@@ -1,35 +1,35 @@
1
- /**
2
- * Deterministic fallback seating: a session id hashes to a stable polar
3
- * seat, so a reload reproduces the same star map even when no layout is
4
- * persisted (saved layouts still win - main.ts checks them first). This
5
- * replaces three Math.random() calls whose only product was a star map
6
- * that reshuffled on every reload, making screenshots and sessions
7
- * incomparable. Same hash family as getHue (FNV-1a 32-bit); zero
8
- * dependencies so the unit test pins stability/bounds without three.
9
- * @module nexus-stage/seat
10
- */
11
-
12
- /** Radial distance bounds, mirroring the historical random-seat envelope. */
13
- export const SEAT_DIST_MIN = 12
14
- export const SEAT_DIST_MAX = 30
15
- /** Vertical jitter bound in world units above/below the grid plane. */
16
- export const SEAT_Y_SPAN = 1
17
-
18
- /** FNV-1a 32-bit - the same determinism family as getHue. */
19
- export function hashId(sid: string): number {
20
- let h = 0x811c9dc5
21
- for (let i = 0; i < sid.length; i++) {
22
- h ^= sid.charCodeAt(i)
23
- h = Math.imul(h, 0x01000193) >>> 0
24
- }
25
- return h >>> 0
26
- }
27
-
28
- /** Deterministic world seat (x, vertical jitter, z) for one session id. */
29
- export function seatFor(sid: string): { x: number; y: number; z: number } {
30
- const h = hashId(sid)
31
- const angle = ((h % 3600) / 3600) * Math.PI * 2
32
- const dist = SEAT_DIST_MIN + (((h >>> 9) % 1800) / 1800) * (SEAT_DIST_MAX - SEAT_DIST_MIN)
33
- const y = (((h >>> 18) % 2000) / 1999) * 2 * SEAT_Y_SPAN - SEAT_Y_SPAN
34
- return { x: Math.cos(angle) * dist, y, z: Math.sin(angle) * dist }
35
- }
1
+ /**
2
+ * Deterministic fallback seating: a session id hashes to a stable polar
3
+ * seat, so a reload reproduces the same star map even when no layout is
4
+ * persisted (saved layouts still win - main.ts checks them first). This
5
+ * replaces three Math.random() calls whose only product was a star map
6
+ * that reshuffled on every reload, making screenshots and sessions
7
+ * incomparable. Same hash family as getHue (FNV-1a 32-bit); zero
8
+ * dependencies so the unit test pins stability/bounds without three.
9
+ * @module nexus-stage/seat
10
+ */
11
+
12
+ /** Radial distance bounds, mirroring the historical random-seat envelope. */
13
+ export const SEAT_DIST_MIN = 12
14
+ export const SEAT_DIST_MAX = 30
15
+ /** Vertical jitter bound in world units above/below the grid plane. */
16
+ export const SEAT_Y_SPAN = 1
17
+
18
+ /** FNV-1a 32-bit - the same determinism family as getHue. */
19
+ export function hashId(sid: string): number {
20
+ let h = 0x811c9dc5
21
+ for (let i = 0; i < sid.length; i++) {
22
+ h ^= sid.charCodeAt(i)
23
+ h = Math.imul(h, 0x01000193) >>> 0
24
+ }
25
+ return h >>> 0
26
+ }
27
+
28
+ /** Deterministic world seat (x, vertical jitter, z) for one session id. */
29
+ export function seatFor(sid: string): { x: number; y: number; z: number } {
30
+ const h = hashId(sid)
31
+ const angle = ((h % 3600) / 3600) * Math.PI * 2
32
+ const dist = SEAT_DIST_MIN + (((h >>> 9) % 1800) / 1800) * (SEAT_DIST_MAX - SEAT_DIST_MIN)
33
+ const y = (((h >>> 18) % 2000) / 1999) * 2 * SEAT_Y_SPAN - SEAT_Y_SPAN
34
+ return { x: Math.cos(angle) * dist, y, z: Math.sin(angle) * dist }
35
+ }
@@ -1,13 +1,13 @@
1
- /**
2
- * Display name for one stage node: the session title when present, then the
3
- * routable team label (`team/<id8>`), then the host fallback label
4
- * (`dsh-host-<node>-<id8>`). Untitled cold joined sessions carry no title and
5
- * without the middle step the canvas drowned in dsh-host-* noise (2026-08-30
6
- * user report: the planning canvas showed a dozen unreadable label cards).
7
- * @module nexus-stage/stage-name
8
- */
9
-
10
- /** One node's display title. */
11
- export function displayName(node: { readonly name?: string; readonly team?: string; readonly label?: string }): string {
12
- return node.name ?? node.team ?? node.label ?? ''
13
- }
1
+ /**
2
+ * Display name for one stage node: the session title when present, then the
3
+ * routable team label (`team/<id8>`), then the host fallback label
4
+ * (`dsh-host-<node>-<id8>`). Untitled cold joined sessions carry no title and
5
+ * without the middle step the canvas drowned in dsh-host-* noise (2026-08-30
6
+ * user report: the planning canvas showed a dozen unreadable label cards).
7
+ * @module nexus-stage/stage-name
8
+ */
9
+
10
+ /** One node's display title. */
11
+ export function displayName(node: { readonly name?: string; readonly team?: string; readonly label?: string }): string {
12
+ return node.name ?? node.team ?? node.label ?? ''
13
+ }
@@ -1,32 +1,32 @@
1
- /**
2
- * A2A Nexus design tokens.
3
- * Three-layer: primitive(C) -> semantic(S) -> component-level inline styles.
4
- */
5
-
6
- export const C = {
7
- bg0: '#060a12',
8
- bg1: '#0d1322',
9
- ink: '#dde3f0',
10
- inkMuted: '#7a86a0',
11
- accent: '#8b5cf6',
12
- liveGreen: '#4ade80',
13
- coldGrey: '#4a5568',
14
- lineFederal: '#6366f1',
15
- membershipLine: '#22d3ee',
16
- } as const;
17
-
18
- /**
19
- * Semantic layer: the scene-facing names the viewer actually consumes,
20
- * each pinned to its primitive above. This is the `S` main.ts reached for
21
- * as `T.S.*` — the layer was documented but never exported, so every seat
22
- * attempt threw and cycle()'s empty catch buried it: an eternally empty
23
- * stage behind a healthy-looking render loop.
24
- */
25
- export const S = {
26
- /** Mesh color + emissive for live session nodes (primitive liveGreen). */
27
- nodeLive: 0x4ade80,
28
- /** Mesh color + emissive for cold/joined-offline nodes (primitive coldGrey). */
29
- nodeCold: 0x4a5568,
30
- } as const;
31
-
1
+ /**
2
+ * A2A Nexus design tokens.
3
+ * Three-layer: primitive(C) -> semantic(S) -> component-level inline styles.
4
+ */
5
+
6
+ export const C = {
7
+ bg0: '#060a12',
8
+ bg1: '#0d1322',
9
+ ink: '#dde3f0',
10
+ inkMuted: '#7a86a0',
11
+ accent: '#8b5cf6',
12
+ liveGreen: '#4ade80',
13
+ coldGrey: '#4a5568',
14
+ lineFederal: '#6366f1',
15
+ membershipLine: '#22d3ee',
16
+ } as const;
17
+
18
+ /**
19
+ * Semantic layer: the scene-facing names the viewer actually consumes,
20
+ * each pinned to its primitive above. This is the `S` main.ts reached for
21
+ * as `T.S.*` — the layer was documented but never exported, so every seat
22
+ * attempt threw and cycle()'s empty catch buried it: an eternally empty
23
+ * stage behind a healthy-looking render loop.
24
+ */
25
+ export const S = {
26
+ /** Mesh color + emissive for live session nodes (primitive liveGreen). */
27
+ nodeLive: 0x4ade80,
28
+ /** Mesh color + emissive for cold/joined-offline nodes (primitive coldGrey). */
29
+ nodeCold: 0x4a5568,
30
+ } as const;
31
+
32
32
  export const FRAME_HUES = [0x8b5cf6, 0x3b82f6, 0x14b8a6, 0xf59e0b, 0xec4899, 0x84cc16];