feat(web): wire startup Workspace selection and sync docs

- Mount WorkspacesService.startInitialSelection in the runtime apply (the
  one-shot baseline follower shipped in 98633b5aa without a caller): a
  restored current session wins, an explicit clear stays cleared, a failed
  connect retries on the next baseline projection.
- Cover the policy in client-apply and the assembled workspace-flow
  snapshot; startup now lands in the recent Workspace's blank session, so
  the draft-carry scenario starts from the hero directly.
- Bring docs along: startup-selection paragraphs in the session-scope RFC
  note (both languages), bilingual README pairs for the four new client
  packages, doc-graph regeneration with client-declared events exempt from
  the dispatcher requirement (client dispatch sites are structurally
  invisible to the host-side ts.Program), and pairing re-records.
This commit is contained in:
imccyu
2026-07-27 06:24:09 +08:00
parent 22e4c05e69
commit cbe8735d7c
28 changed files with 372 additions and 96 deletions

View File

@@ -703,7 +703,7 @@ class EventRelationCollector {
const eventNames = this.eventNamesFromCall(node, receiverKind)
if (method === 'on' || method === 'once') {
for (const event of eventNames) this.ensure(event).listeners.add(source.pkg)
} else if (method === 'emit' || method === 'parallel' || method === 'serial' || method === 'waterfall' || method === 'bail') {
} else if (method === 'emit' || method === 'parallel' || method === 'serial' || method === 'waterfall') {
for (const event of eventNames) this.addDispatcher(event, source.pkg, method)
}
}
@@ -917,8 +917,13 @@ function renderEventRelations(pkgs: Pkg[]): string {
lines.push(`| \`${event.name}\` | \`${event.mode}\` | ${sourceLink(event.source)} | ${relationPackages(relation.dispatchers, pkgsByShort)} | ${listenerPackages(relation.listeners, pkgsByShort)} |`)
}
// Every declared event needs a dispatcher: zero means dead vocabulary or an
// unrecognized semantic dispatch shape. Listener-free extension points remain valid.
// unrecognized semantic dispatch shape. Listener-free extension points remain
// valid. Client-declared events are exempt: the relation scan seeds the HOST
// aggregate program only (host+client cannot share one program — the cordis
// Context merges collide), so client dispatch sites are structurally
// invisible here; their rows stay in the table for the declarations' sake.
const undispatched = [...events]
.filter(event => !event.source.startsWith('packages/client/'))
.filter(event => (relations.get(event.name)?.dispatchers.size ?? 0) === 0)
.map(event => event.name)
.sort()