diff --git a/AGENTS.md b/AGENTS.md index fb20274f71..4024eabf70 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -36,6 +36,7 @@ python/ Python SDK and bundled runtime (see python/README.md) examples/ Runnable cordis.yml leaves over packages/examples bundles (see examples/AGENTS.md) docs/ architecture, generated catalogs, RFCs, postmortems, cookbook (see docs/AGENTS.md) scripts/ repo gates and generators +website/ VitePress docs site (zh-CN); api/ pages generated from source ``` Package groups: [packages/README.md](packages/README.md). @@ -55,6 +56,7 @@ pnpm run duplication # cross-file TypeScript clone detection pnpm run build # tsc emits lib/types, tsdown bundles runtime pnpm run hygiene # knip + publint + workspace constraints + NodeNext consumer check pnpm run doc-sync # all documentation gates; see the doc-sync script in package.json +pnpm run website:build # VitePress build (doubles as the site's dead-link check) pnpm run demo:echo # mock-model REPL, no key needed pnpm run demo:repl # real REPL coding agent (needs DEEPSEEK_API_KEY) pnpm run demo:cordis # self-referential demo: the agent modifies its own runtime (needs key) @@ -77,6 +79,7 @@ pnpm run duplication pnpm run test:coverage pnpm run test:snapshot pnpm run doc-sync +pnpm run website:build pnpm run verify-module-graph pnpm run build pnpm run hygiene diff --git a/docs/cordis-catalog/events.md b/docs/cordis-catalog/events.md index 2069c92335..dcab7856d8 100644 --- a/docs/cordis-catalog/events.md +++ b/docs/cordis-catalog/events.md @@ -259,7 +259,7 @@ Creation announcement during session publication. A synchronous throw vetoes and 'session/created'(this: Scoped, session: Session): void ``` -Source: [`packages/core/session/src/index.ts:47`](../../packages/core/session/src/index.ts) +Source: [`packages/core/session/src/index.ts:46`](../../packages/core/session/src/index.ts) ### `session/disposed` — emit @@ -269,7 +269,7 @@ Emitted once when an announced session leaves the store, including publication r 'session/disposed'(this: Scoped, session: Session): void ``` -Source: [`packages/core/session/src/index.ts:57`](../../packages/core/session/src/index.ts) +Source: [`packages/core/session/src/index.ts:56`](../../packages/core/session/src/index.ts) ### `session/event` — emit @@ -281,7 +281,7 @@ Post-commit, fire-and-forget append feed. The listener snapshot resolves before Types: [SessionEvent](../core-data-structures/core.md) -Source: [`packages/core/session/src/index.ts:69`](../../packages/core/session/src/index.ts) +Source: [`packages/core/session/src/index.ts:68`](../../packages/core/session/src/index.ts) ### `session/flush` — parallel @@ -291,7 +291,7 @@ Awaited parallel durability checkpoint: every listener runs and the caller await 'session/flush'(this: Scoped, session: Session): Promise | void ``` -Source: [`packages/core/session/src/index.ts:79`](../../packages/core/session/src/index.ts) +Source: [`packages/core/session/src/index.ts:78`](../../packages/core/session/src/index.ts) ## `subagent/*` @@ -483,14 +483,14 @@ Source: [`packages/workflow/workflow/src/index.ts:45`](../../packages/workflow/w The framework events every plugin also sees, beyond the harness vocabulary above. This is pinned vendor source ([vendoring policy](../../vendor/README.md)); it is summarized here so the page is a complete picture of the event bus, without elevating framework internals to the harness tier's prominence. -- `internal/plugin` — A plugin fiber was created. ([`vendor/cordis/src/events.ts:197`](../../vendor/cordis/src/events.ts)) -- `internal/status` — A fiber changed lifecycle state. ([`vendor/cordis/src/events.ts:198`](../../vendor/cordis/src/events.ts)) -- `internal/service` — Interception hook for a service binding (no core producer). ([`vendor/cordis/src/events.ts:199`](../../vendor/cordis/src/events.ts)) -- `internal/update` — Waterfall: a fiber config update is being applied. ([`vendor/cordis/src/events.ts:200`](../../vendor/cordis/src/events.ts)) -- `internal/get` — Waterfall: a service is being read from the store. ([`vendor/cordis/src/events.ts:201`](../../vendor/cordis/src/events.ts)) -- `internal/set` — Waterfall: a service is being written to the store. ([`vendor/cordis/src/events.ts:202`](../../vendor/cordis/src/events.ts)) -- `internal/listener` — A listener was registered. ([`vendor/cordis/src/events.ts:203`](../../vendor/cordis/src/events.ts)) -- `internal/dispatch` — An event is being dispatched to listeners. ([`vendor/cordis/src/events.ts:204`](../../vendor/cordis/src/events.ts)) +- `internal/plugin` — A plugin fiber was created. ([`vendor/cordis/src/events.ts:328`](../../vendor/cordis/src/events.ts)) +- `internal/status` — A fiber changed lifecycle state. ([`vendor/cordis/src/events.ts:330`](../../vendor/cordis/src/events.ts)) +- `internal/service` — Interception hook for a service binding (no core producer). ([`vendor/cordis/src/events.ts:332`](../../vendor/cordis/src/events.ts)) +- `internal/update` — Waterfall: a fiber config update is being applied. ([`vendor/cordis/src/events.ts:334`](../../vendor/cordis/src/events.ts)) +- `internal/get` — Waterfall: a service is being read from the store. ([`vendor/cordis/src/events.ts:336`](../../vendor/cordis/src/events.ts)) +- `internal/set` — Waterfall: a service is being written to the store. ([`vendor/cordis/src/events.ts:338`](../../vendor/cordis/src/events.ts)) +- `internal/listener` — A listener was registered. ([`vendor/cordis/src/events.ts:340`](../../vendor/cordis/src/events.ts)) +- `internal/dispatch` — An event is being dispatched to listeners. ([`vendor/cordis/src/events.ts:342`](../../vendor/cordis/src/events.ts)) - `hmr/change` — A watched source file changed on disk. ([`vendor/hmr/src/index.ts:20`](../../vendor/hmr/src/index.ts)) - `hmr/reload` — Plugins are being reloaded after a change. ([`vendor/hmr/src/index.ts:21`](../../vendor/hmr/src/index.ts)) - `exit` — The process is exiting on a signal. ([`vendor/loader/src/index.ts:23`](../../vendor/loader/src/index.ts)) diff --git a/docs/cordis-catalog/services.md b/docs/cordis-catalog/services.md index d780c4d878..7f9260bcae 100644 --- a/docs/cordis-catalog/services.md +++ b/docs/cordis-catalog/services.md @@ -111,7 +111,7 @@ abstract compactRegion( session: Session, start: number, end: number, agent: Com Types: [Message](../core-data-structures/core.md) -Source: [`packages/compact/compact/src/index.ts:36`](../../packages/compact/compact/src/index.ts) +Source: [`packages/compact/compact/src/index.ts:37`](../../packages/compact/compact/src/index.ts) ## `ctx.fs` — `FileSystem` (abstract seam) @@ -234,7 +234,7 @@ list(): Session[] fork(source: SessionForkSource, boundary?: number, childSessionId?: SessionId): Session ``` -Source: [`packages/core/session/src/index.ts:540`](../../packages/core/session/src/index.ts) +Source: [`packages/core/session/src/index.ts:539`](../../packages/core/session/src/index.ts) ## `ctx.skills` — `SkillService` @@ -374,12 +374,12 @@ Source: [`packages/workflow/workflow/src/index.ts:159`](../../packages/workflow/ The framework `ctx` surface every plugin also sees, beyond the harness services above. This is pinned vendor source ([vendoring policy](../../vendor/README.md)); it is summarized here so the page is a complete picture of what `ctx` offers, without elevating framework internals to the harness tier's prominence. -- `ctx.on / ctx.once` — Register an event listener (disposable). ([`vendor/cordis/src/events.ts:29`](../../vendor/cordis/src/events.ts)) -- `ctx.emit / ctx.parallel / ctx.serial / ctx.bail / ctx.waterfall` — Dispatch an event (sync / awaited / first-bail / veto-chain). ([`vendor/cordis/src/events.ts:29`](../../vendor/cordis/src/events.ts)) -- `ctx.plugin / ctx.inject` — Load a plugin / declare required services. ([`vendor/cordis/src/registry.ts:144`](../../vendor/cordis/src/registry.ts)) +- `ctx.on / ctx.once` — Register an event listener (disposable). ([`vendor/cordis/src/events.ts:34`](../../vendor/cordis/src/events.ts)) +- `ctx.emit / ctx.parallel / ctx.serial / ctx.bail / ctx.waterfall` — Dispatch an event (sync / awaited / first-bail / veto-chain). ([`vendor/cordis/src/events.ts:34`](../../vendor/cordis/src/events.ts)) +- `ctx.plugin / ctx.inject` — Load a plugin / declare required services. ([`vendor/cordis/src/registry.ts:164`](../../vendor/cordis/src/registry.ts)) - `ctx.effect` — Register a disposable side effect tied to the fiber. ([`vendor/cordis/src/fiber.ts:9`](../../vendor/cordis/src/fiber.ts)) - `ctx.get / ctx.set / ctx.provide / ctx.accessor / ctx.mixin` — Low-level service-store access and binding. ([`vendor/cordis/src/reflect.ts:7`](../../vendor/cordis/src/reflect.ts)) -- `ctx.extend / ctx.isolate / ctx.intercept` — Derive a child context (scoped services / isolation / interception). ([`vendor/cordis/src/context.ts:35`](../../vendor/cordis/src/context.ts)) +- `ctx.extend / ctx.isolate / ctx.intercept` — Derive a child context (scoped services / isolation / interception). ([`vendor/cordis/src/context.ts:42`](../../vendor/cordis/src/context.ts)) - `ctx.root / ctx.scope / ctx.fiber / ctx.registry / ctx.reflect / ctx.events / ctx.logger` — Ambient handles onto the running context graph. ([`vendor/cordis/src/context.ts:16`](../../vendor/cordis/src/context.ts)) - `ctx.timer (+ interval / timeout / throttle / debounce / setTimeout / setInterval)` — Disposable timer helpers. The `timer` key is provided at runtime; the six helpers are mixed onto ctx directly (declared via Pick). ([`vendor/timer/src/index.ts:4`](../../vendor/timer/src/index.ts)) - `ctx.loader` — The config Loader that booted the app (present under the loader). ([`vendor/loader/src/index.ts:30`](../../vendor/loader/src/index.ts)) diff --git a/docs/core-data-structures/compaction.md b/docs/core-data-structures/compaction.md index 82bf512eeb..05022f9937 100644 --- a/docs/core-data-structures/compaction.md +++ b/docs/core-data-structures/compaction.md @@ -53,3 +53,5 @@ interface CompactionResult { `CompactService` exposes `compactIfNeeded(...)` for pressure-triggered compaction, returning `null` when no compaction is needed, and `compactRegion(...)` for an explicit inclusive surface range. The pre-step caller supplies the agent, full prompt, session prefix, and abort signal; implementations must forward that signal to summarization. Estimation, retention, event sequencing, and summarization remain backend policy. Auto-compaction runs at serial `agent/pre-step`, before the step and request derivation, so it can replace surface nodes while keeping trace events outside the step. Region boundaries preserve tool-call/result pairing but do not preserve whole turns, allowing early closed steps of one oversized turn to compact. `dsh-compact-basic` owns the retention and failure details. + +The seam exports `toolPairingBalancedBefore(session, node)` and `toolPairingBalancedAfter(session, node)` for those edge checks. Both validate current surface membership, reject stale or missing seqs and orphan results, and ignore a caller-retained `node.next`; the [package contract](../../packages/compact/compact/README.md#tool-pairing-boundaries) owns their cache semantics. diff --git a/docs/core-data-structures/session.md b/docs/core-data-structures/session.md index a8738ae431..c928fd30a4 100644 --- a/docs/core-data-structures/session.md +++ b/docs/core-data-structures/session.md @@ -211,6 +211,8 @@ export interface SurfaceNode { } ``` +`SurfaceNode` is positional state, not durable identity. A replacement can remove a caller-retained node or make a copied `next` stale; consumers that cross a surface mutation validate membership and answer positional queries from `Session.surface.nodes`. `SurfaceManager.replaceGeneration` increments for each replacement so incremental consumers can distinguish pure tail growth from a rewrite. + ### `SurfaceFoldReplacement` and `SurfaceFoldResult` — a complete surface replay `foldSurface(events)` returns detached current nodes together with the actual node seqs shadowed by each declared replacement range. `SurfaceManager` uses the same transition functions for its incremental cache. diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md index d096fe3362..3bb83db892 100644 --- a/docs/event-producer-consumer.md +++ b/docs/event-producer-consumer.md @@ -26,10 +26,10 @@ This matrix shows which packages dispatch each harness-owned event and which pac | `fs/observed` | `emit` | [`packages/fs/fs/src/index.ts:70`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`emit`) | [`fs-policy`](../packages/fs/fs-policy) | | `fs/write-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:53`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`) | [`fs-policy`](../packages/fs/fs-policy) | | `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:39`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`invariants`](../packages/support/invariants), [`llm-replay`](../packages/support/llm-replay) | -| `session/created` | `emit` | [`packages/core/session/src/index.ts:47`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`invariants`](../packages/support/invariants), [`jsonrpc`](../packages/ui/jsonrpc), [`session-persistence`](../packages/session-persistence/session-persistence) | -| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:57`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | - | -| `session/event` | `emit` | [`packages/core/session/src/index.ts:69`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/ui/acp), [`invariants`](../packages/support/invariants), [`jsonrpc`](../packages/ui/jsonrpc), [`session-persistence`](../packages/session-persistence/session-persistence), [`stdio`](../packages/ui/stdio), [`workspace-context`](../packages/context/workspace-context) | -| `session/flush` | `parallel` | [`packages/core/session/src/index.ts:79`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session-persistence/session-persistence) | +| `session/created` | `emit` | [`packages/core/session/src/index.ts:46`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`invariants`](../packages/support/invariants), [`jsonrpc`](../packages/ui/jsonrpc), [`session-persistence`](../packages/session-persistence/session-persistence) | +| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:56`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session-persistence/session-persistence) | +| `session/event` | `emit` | [`packages/core/session/src/index.ts:68`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/ui/acp), [`invariants`](../packages/support/invariants), [`jsonrpc`](../packages/ui/jsonrpc), [`session-persistence`](../packages/session-persistence/session-persistence), [`stdio`](../packages/ui/stdio), [`workspace-context`](../packages/context/workspace-context) | +| `session/flush` | `parallel` | [`packages/core/session/src/index.ts:78`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session-persistence/session-persistence) | | `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:108`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`jsonrpc`](../packages/ui/jsonrpc) | | `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:82`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`tool-subagent`](../packages/subagent/tool-subagent) | | `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:88`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`tool-subagent`](../packages/subagent/tool-subagent) | diff --git a/docs/rfc/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md b/docs/rfc/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md index b1773b480d..fef6ad034c 100644 --- a/docs/rfc/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md +++ b/docs/rfc/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md @@ -12,6 +12,8 @@ Extract a backend-agnostic `PersistenceCoordinator` into `dsh-session-persistenc Composition, not inheritance. The coordinator is a concrete class the backend holds, not a base class the backend extends. The RFC's risk — "a coordinator must not make unusual backends fight an inheritance hierarchy" — is avoided: a backend exposes only the hooks; it cannot reach the coordinator's private orchestration state, and the public `SessionPersistence` service shape is unchanged, so a third-party backend MAY still implement the abstract service directly without the coordinator at all. +The coordinator retires each live session from its `session/disposed` notification: it waits for that exact Session object's initialization, serializes a final drain, and then removes the owned state, buffer, and init entries. Failed drains retain their buffers for backend teardown to retry. Settled per-id chain tails remove themselves only when they are still the current tail, so a completion cannot erase a newer operation for the same id. Backend teardown unregisters the write-path listeners before awaiting all admitted retirements, remaining buffers, and chains, then closes the backend. + ### The hook interface (`PersistenceBackend`) Six methods (five required + an optional lifecycle hook) — the only seam between the coordinator and storage: @@ -30,7 +32,7 @@ The single design choice that keeps the seam clean: the crash-repair "where is t ## Testing -The shared `runPersistenceContract` (public-API contract) keeps running for every backend. A new `runCoordinatorContract` (`tests/coordinator-contract.ts`) holds the write-path orchestration — adoption, HMR, collision, dispose-drain, crash-tail repair — and runs once per backend through a `CoordinatorFixture` (an in-memory reference + jsonl + sqlite). The per-backend specs shrank to storage mechanics only (JSONL: path safety, fsync rollback, bucket listing; SQLite: schema version, `scanRows`, transaction rollback). A through-coordinator torn-tail→load→`commitRepair` test per real backend (via a `corruptTail` fixture hook) keeps the coordinator's torn-marker repair branch covered under the 100% per-file gate — the contract crash test only produces synthetic closers, never a torn marker, so it could not reach that branch. +The shared `runPersistenceContract` (public-API contract) keeps running for every backend. `runCoordinatorContract` (`tests/coordinator-contract.ts`) holds the write-path orchestration — adoption, HMR, collision, session and backend disposal drains, and crash-tail repair — and runs once per backend through a `CoordinatorFixture` (an in-memory reference + jsonl + sqlite). Coordinator-specific tests pin retirement map cleanup, same-id chain-tail races, failed-drain retry, and close ordering. The per-backend specs retain storage mechanics only (JSONL: path safety, fsync rollback, bucket listing; SQLite: schema version, `scanRows`, transaction rollback). A through-coordinator torn-tail→load→`commitRepair` test per real backend (via a `corruptTail` fixture hook) keeps the coordinator's torn-marker repair branch covered under the 100% per-file gate — the contract crash test only produces synthetic closers, never a torn marker, so it could not reach that branch. ## Alternatives considered @@ -39,4 +41,4 @@ The shared `runPersistenceContract` (public-API contract) keeps running for ever ## Consequences -The coordinator adds one indirection and an opaque torn marker, but centralizes correctness-heavy orchestration previously duplicated by every backend. Its hook surface stays narrow: collision checks reuse `loadStored`, materialization stays atomic inside `appendBatch`, and listing bypasses the coordinator. New backends implement storage primitives rather than copy the event-buffer-flush lifecycle. +The coordinator adds one indirection, an opaque torn marker, and detached session-retirement tasks, but centralizes correctness-heavy orchestration previously duplicated by every backend. Session disposal remains an observe-only event, so the session owner does not await persistence retirement; the coordinator contains failures, preserves uncommitted buffers, and makes backend teardown the quiescence boundary. Its hook surface stays narrow: collision checks reuse `loadStored`, materialization stays atomic inside `appendBatch`, and listing bypasses the coordinator. New backends implement storage primitives rather than copy the event-buffer-flush lifecycle. diff --git a/docs/rfc/implemented/feature/2026-06-18-compaction-capability-seam.md b/docs/rfc/implemented/feature/2026-06-18-compaction-capability-seam.md index 4a6586b159..75eaf3207f 100644 --- a/docs/rfc/implemented/feature/2026-06-18-compaction-capability-seam.md +++ b/docs/rfc/implemented/feature/2026-06-18-compaction-capability-seam.md @@ -52,7 +52,7 @@ The loop derives messages once after `agent/pre-step`. Running before `step/star Auto-compaction fires before **every** step, not once per turn. This is **load-bearing for runaway-turn survival**: a tool-heavy ReAct turn appends an `assistant/message` + a `tool/result` per step, so the surface grows *within* a turn. A single turn can grow past the window on its own (a "runaway turn") — and the only moment to rescue it before the next model call overflows is the next step's `pre-step` checkpoint. Gating compaction to a turn's first step (or, worse, retaining the whole in-flight turn verbatim) re-opens exactly the hole compaction exists to close: the harness would die when compaction is most needed. -`compactIfNeeded` retains the smallest tail of whole surface units whose estimated size reaches `retainTokens` and compacts older nodes. A unit is a complete closed step or one no-step message. If the token cutoff lands inside a step, retention expands until the cut is tool-pairing balanced. Balance is checked on surface order, not log sequence, because replacement summaries have new sequence numbers at old surface positions. `compactRegion` rejects boundaries that split a tool call from its result. The in-flight turn receives no special retention. +`compactIfNeeded` retains the smallest tail of whole surface units whose estimated size reaches `retainTokens` and compacts older nodes. A unit is a complete closed step or one no-step message. If the token cutoff lands inside a step, retention expands until the cut is tool-pairing balanced. Balance is checked on surface order, not log sequence, because replacement summaries have new sequence numbers at old surface positions. `dsh-compact` exports the before/after edge helpers; their per-session cache folds only appended surface-tail nodes while `replaceGeneration` is unchanged, does no event reads for log-only growth, and rebuilds current membership and balances after replacement. `compactRegion` rejects boundaries that split a tool call from its result. The in-flight turn receives no special retention. A runaway turn thus compacts exactly like any other history: its early *closed* steps get summarized while its recent steps stay verbatim. When the only compactable content left is an un-splittable open tail step (its tool-calls have no results yet), compaction declines (`null`) and retries once that step closes. @@ -113,7 +113,7 @@ Two failure paths, both documented: - **New packages**: `packages/compact/compact` (interface) and a sibling `compact-basic` (backend) under `packages/compact/`, wired into the root tsconfigs. The consumer tier is deferred. - **New loop seam**: `agent/pre-step` (`@mode serial`) declared in `dsh-agent` and emitted by `dsh-agent-loop` after system assembly and before `step/start`. This is a documented change to the loop — `docs/architecture.md` records it and the generated cordis catalog carries its signature. - **`SessionEventMap`** gains `compact/start` / `compact/summary` / `compact/end` by declaration merging (merge-extensible); `SurfaceEventType` is **not** touched. These are session events, not cordis `Events`, so the event-taxonomy gate needs no entry. -- **`dsh-session`** gains the tool-pairing balance predicate (`isToolPairingBalanced`, in `tool-pairing.ts`, exported from the package index) that `compactRegion`/`compactIfNeeded` use to keep a collapsed region from splitting a step's tool-call/result pair. The surface `replace` op and the surface-metadata runtime guard already existed and are reused. +- **`dsh-compact`** owns `toolPairingBalancedBefore(session, node)` and `toolPairingBalancedAfter(session, node)`, the cached surface-edge checks that `compactRegion` and `compactIfNeeded` use to avoid splitting a tool-call/result pair. The cache validates current membership by seq and answers both edges from one per-cut balance sequence instead of trusting a caller-retained `node.next`; stale or missing seqs and orphan results reject. `dsh-session` continues to own the surface `replace` operation, positional nodes, and rewrite generation. - **`dsh-invariants`** drops its `surface replace: start must be <= end` assertion: a head-anchored compaction lands a high-seq replacement node at an older range's *position*, so `start > end` numerically is normal and valid (the range is positional, validated by the surface's `indexOf` checks that remain). The turn-enclosure invariant is reused unchanged. - **Wiring**: `dsh-compact-basic` is loaded in `examples/coding-agent`'s `cordis.yml`, so the seam ships in the real demo (it was previously loaded nowhere). diff --git a/docs/rfc/proposed/simplification/2026-07-12-simplify-session-log-representation.md b/docs/rfc/proposed/simplification/2026-07-12-simplify-session-log-representation.md index 715ce93924..1cb8d5708a 100644 --- a/docs/rfc/proposed/simplification/2026-07-12-simplify-session-log-representation.md +++ b/docs/rfc/proposed/simplification/2026-07-12-simplify-session-log-representation.md @@ -6,7 +6,7 @@ Status: proposed The session log maintains two representations that cost more machinery than their consumers require: a pseudo-linked surface and custom request-header deltas. -`SurfaceManager` stores the same order in an array, a seq map, and mutable `prev`/`next` links. Production never reads `prev`; compact's sole `next` read is the successor of an array position. Replacement already uses `indexOf`, so the links do not make its dominant operation constant-time. A seq array with linear replacement lookup has the same asymptotic replacement cost and one representation to validate. +`SurfaceManager` stores the same order in an array, a seq map, and mutable `prev`/`next` links. Production never reads either link: compact's tool-pairing balance answers from per-cut balances cached in surface order. Replacement already uses `indexOf`, so the links do not make its dominant operation constant-time. A seq array with linear replacement lookup has the same asymptotic replacement cost and one representation to validate. The request-header subsystem implements a custom system/tool delta codec and transmission-decision layer even though its contract says deltas are an encoding optimization, not a reconstructability requirement. Retaining the initial/resume full snapshot at each loop-instance boundary, then writing a canonical full `request/header` whenever that instance's assembled header changes, preserves replay while deleting `SystemDelta`, `ToolsDelta`, round-trip fallback, and the durable `request/header-delta` variant. Codec-only vocabulary disappears with the codec, not because its individual arms were invalid. diff --git a/knip.json b/knip.json index 58278f286b..9c644cebb7 100644 --- a/knip.json +++ b/knip.json @@ -2,7 +2,7 @@ "$schema": "https://unpkg.com/knip@5/schema.json", "exclude": ["duplicates"], "ignoreBinaries": ["bwrap", "sandbox-exec"], - "ignoreWorkspaces": ["vendor/*", "python/sdk-runtime"], + "ignoreWorkspaces": ["vendor/*", "python/sdk-runtime", "website"], "workspaces": { ".": { "entry": [ diff --git a/package.json b/package.json index 3438af15d8..84a59de3da 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ }, "workspaces": [ "vendor/*", - "packages/*/*" + "packages/*/*", + "website" ], "scripts": { "build": "tsc -b tsconfig.build.json && tsdown", @@ -69,8 +70,13 @@ "gen-scoped-events": "tsx scripts/gen-scoped-events.ts", "verify-scoped-events": "tsx scripts/gen-scoped-events.ts --check", "verify-module-graph": "tsx scripts/gen-module-graph.ts --check", + "gen-website-api": "tsx scripts/gen-website-api.ts", + "verify-website-api": "tsx scripts/gen-website-api.ts --check", + "verify-website-yaml": "tsx scripts/verify-website-yaml.ts", + "website:dev": "pnpm --filter @deepseek-ai/website run dev", + "website:build": "pnpm --filter @deepseek-ai/website run build", "constraints": "tsx scripts/check-workspace-constraints.ts", - "doc-sync": "pnpm run doc-typecheck && pnpm run verify-cordis-catalog && pnpm run verify-cordis-api && pnpm run verify-export-jsdoc && pnpm run verify-tool-catalog && pnpm run verify-config-catalog && pnpm run verify-persistence-catalog && pnpm run verify-doc-graphs && pnpm run verify-scoped-events && pnpm run verify-md-wrap && pnpm run verify-md-links && pnpm run verify-doc-refs && pnpm run verify-package-paths && pnpm run verify-package-readme-model-experience && pnpm run verify-mermaid && pnpm run verify-rfc-classification && pnpm run verify-rfc-format && pnpm run verify-type-equiv && pnpm run verify-translation-prompt && pnpm run verify-translation-pairing && pnpm run verify-doc-budgets && pnpm run verify-package-readme-limitations", + "doc-sync": "pnpm run doc-typecheck && pnpm run verify-cordis-catalog && pnpm run verify-cordis-api && pnpm run verify-export-jsdoc && pnpm run verify-tool-catalog && pnpm run verify-config-catalog && pnpm run verify-persistence-catalog && pnpm run verify-doc-graphs && pnpm run verify-scoped-events && pnpm run verify-website-api && pnpm run verify-md-wrap && pnpm run verify-md-links && pnpm run verify-doc-refs && pnpm run verify-package-paths && pnpm run verify-package-readme-model-experience && pnpm run verify-mermaid && pnpm run verify-rfc-classification && pnpm run verify-rfc-format && pnpm run verify-type-equiv && pnpm run verify-translation-prompt && pnpm run verify-translation-pairing && pnpm run verify-doc-budgets && pnpm run verify-package-readme-limitations && pnpm run verify-website-yaml", "hygiene": "pnpm run knip && pnpm run publint && pnpm run constraints && pnpm run verify-cordis-config && pnpm run verify-node-next-types && pnpm run verify-runtime-closure", "demo:echo": "node --expose-internals --import tsx packages/examples/stdio-demo/src/bin.ts examples/echo-agent/cordis.yml", "demo:repl": "node --expose-internals --import tsx packages/examples/stdio-demo/src/bin.ts examples/coding-agent/cordis.yml", diff --git a/packages/compact/compact-basic/README.md b/packages/compact/compact-basic/README.md index d9c0f472a8..d05ce47356 100644 --- a/packages/compact/compact-basic/README.md +++ b/packages/compact/compact-basic/README.md @@ -9,7 +9,7 @@ This is the implementation tier of the compaction capability — see the [interf This backend owns the compaction policy: - **Estimation** — a configurable characters-per-token heuristic counts the current session prefix supplied to pre-step, derived history, and system prompt, matching the next request rather than stale logged prefix state. -- **Retention** — compact the oldest whole surface units while preserving a recent tail and balanced tool-call/result cuts. Turn boundaries do not protect old steps inside a runaway turn. An open indivisible tail declines until it closes; a single unit larger than the budget remains out of scope. +- **Retention** — compact the oldest whole surface units while preserving a recent tail and balanced tool-call/result cuts through the [`dsh-compact` boundary helpers](../compact/README.md#tool-pairing-boundaries). Turn boundaries do not protect old steps inside a runaway turn. An open indivisible tail declines until it closes; a single unit larger than the budget remains out of scope. - **Convergence** — retry head-checkpoint compaction up to `compactionRetries`; reject a summary that does not shrink its source, and throw if retries cannot return below threshold. - **Summarization** — a direct `llm/stream` call uses the configured model and cap without running the loop-only `agent/request` seam. The input transcript preserves non-text blocks as tagged placeholders; only returned text enters the checkpoint, excluding reasoning and tool calls that would leak private reasoning or create an orphaned call. - **Framing** — the replacement user message marks established checkpoint context with `` tags. The raw summary remains on the provenance event, and later automatic cycles merge the prior checkpoint. diff --git a/packages/compact/compact-basic/src/index.ts b/packages/compact/compact-basic/src/index.ts index bf349f7f73..fffc8e9a63 100644 --- a/packages/compact/compact-basic/src/index.ts +++ b/packages/compact/compact-basic/src/index.ts @@ -7,12 +7,11 @@ */ import { Context } from 'cordis' -import { CompactService, renderTranscript } from '@deepseek-ai/dsh-compact' +import { CompactService, renderTranscript, toolPairingBalancedAfter, toolPairingBalancedBefore } from '@deepseek-ai/dsh-compact' import type { CompactionResult } from '@deepseek-ai/dsh-compact' import { BlockAssembler } from '@deepseek-ai/dsh-llm' import type { ContentBlock, FinishReason, GenerateOptions, Message } from '@deepseek-ai/dsh-llm' import type { Session, SessionEvent } from '@deepseek-ai/dsh-session' -import { isToolPairingBalanced } from '@deepseek-ai/dsh-session' import type { Agent } from '@deepseek-ai/dsh-agent' import type { BasicCompactConfig, ResolvedConfig } from './types.ts' import { resolveConfig } from './types.ts' @@ -348,15 +347,14 @@ export class BasicCompactService extends CompactService { } // Both range edges must preserve assistant tool-call/result pairing. - const events = session.events - if (!isToolPairingBalanced(nodes, events, start)) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const startNode = nodes[startIdx]! + if (!toolPairingBalancedBefore(session, startNode)) { throw new Error(`compactRegion: start seq ${start} is not a balanced boundary (would split a step's tool-call/result pair)`) } - // The cut after `end` is named by `end`'s surface successor, or `null` when - // `end` is the tail. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const afterEnd: number | null = nodes[endIdx]!.next - if (!isToolPairingBalanced(nodes, events, afterEnd)) { + const endNode = nodes[endIdx]! + if (!toolPairingBalancedAfter(session, endNode)) { throw new Error(`compactRegion: end seq ${end} is not a balanced boundary (would split a step, or the step is still open)`) } @@ -511,7 +509,7 @@ export class BasicCompactService extends CompactService { // splitting an assistant↔result pair. while (keepFromIdx > 0) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - if (isToolPairingBalanced(nodes, events, nodes[keepFromIdx]!.seq)) break + if (toolPairingBalancedBefore(session, nodes[keepFromIdx]!)) break keepFromIdx -= 1 } if (keepFromIdx === 0) return null diff --git a/packages/compact/compact-basic/tests/compact-loop-repro.spec.ts b/packages/compact/compact-basic/tests/compact-loop-repro.spec.ts index f2d1a60ad8..0fe7ef77f8 100644 --- a/packages/compact/compact-basic/tests/compact-loop-repro.spec.ts +++ b/packages/compact/compact-basic/tests/compact-loop-repro.spec.ts @@ -1,8 +1,8 @@ import { describe, expect, it } from 'vitest' import { Context } from 'cordis' +import { toolPairingBalancedAfter, toolPairingBalancedBefore } from '@deepseek-ai/dsh-compact' import type { ContentBlock, GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm' import { CallId, LlmAdapter } from '@deepseek-ai/dsh-llm' -import { isToolPairingBalanced } from '@deepseek-ai/dsh-session' import { defineTool } from '@deepseek-ai/dsh-tools' import { AgentId } from '@deepseek-ai/dsh-agent' import AgentLoop, { ReactLoopAgent } from '@deepseek-ai/dsh-agent-loop' @@ -118,9 +118,9 @@ describe('CBR-001: a real-loop checkpoint is a valid boundary on both sides', () for (const cp of checkpoints) { const node = nodes.find(n => n.seq === cp.seq) if (!node) continue // shadowed by a later checkpoint — no longer an edge. - expect(isToolPairingBalanced(nodes, events, node.seq), + expect(toolPairingBalancedBefore(agent.session, node), `checkpoint seq ${node.seq} must be a balanced region START`).toBe(true) - expect(isToolPairingBalanced(nodes, events, node.next), + expect(toolPairingBalancedAfter(agent.session, node), `checkpoint seq ${node.seq} must be a balanced region END`).toBe(true) } } finally { diff --git a/packages/compact/compact/README.md b/packages/compact/compact/README.md index 9141012281..ef8c300856 100644 --- a/packages/compact/compact/README.md +++ b/packages/compact/compact/README.md @@ -6,7 +6,7 @@ This package is the interface tier of the compaction capability, split so each c | Package | Role | |---|---| -| `@deepseek-ai/dsh-compact` (this) | the interface: abstract service + `compact/*` events + `CompactionResult` + the shared transcript renderer (`renderTranscript`/`renderContentBlocks`) | +| `@deepseek-ai/dsh-compact` (this) | the interface: abstract service + `compact/*` events + `CompactionResult` + tool-pairing boundary helpers + the shared transcript renderer (`renderTranscript`/`renderContentBlocks`) | | `@deepseek-ai/dsh-compact-basic` | a backend: chars-per-token estimation (`charsPerToken`, default 4) + token-budget retention + `llm.stream()` summarization | | `@deepseek-ai/dsh-tool-compact` (deferred) | the model-facing `/compact` tool over `ctx.compact` | @@ -23,6 +23,12 @@ Both methods are **abstract** — the backend owns the entire strategy (token es `compactIfNeeded` takes a required `signal`; `compactRegion`'s is optional. A backend that summarizes via `ctx.llm.stream()` **must** forward it into the call's `GenerateOptions.signal`, so an abort or fiber dispose tears down the in-flight summarization instead of leaving an orphaned model call running past the cancellation. The session being compacted comes from the agent context; the turn that the `compact/*` events belong to is recoverable from the log (the currently-open turn), so the backend stamps it from the log rather than trusting a caller-supplied value. +## Tool-pairing boundaries + +The interface exports `toolPairingBalancedBefore(session, node)` and `toolPairingBalancedAfter(session, node)` for snapping and validating compaction edges. A safe edge has no unanswered assistant tool call crossing it. Each helper identifies the node by seq alone and answers from balances cached per cut in current surface order, so a stale caller-held `node.next` cannot choose the cut. + +The private per-session cache is keyed by `session.surface.replaceGeneration` and the processed surface-node count. An unchanged generation extends the fold with unseen tail nodes only; a log-only append with no new surface node does no event reads, while a replacement generation rebuilds current membership and balances. Missing event seqs and a `tool/result` without a preceding open call reject as corrupt surface state. + ## Surface contract `SurfaceEventType` is a closed union — only `user/message`, `assistant/message`, `tool/result`, `context/message`, and `steering/message` may carry `surfaceOp`. A `compact/*` event therefore **cannot** appear on the surface. A successful compaction instead: diff --git a/packages/compact/compact/src/index.ts b/packages/compact/compact/src/index.ts index f59e0dc328..12eb77b362 100644 --- a/packages/compact/compact/src/index.ts +++ b/packages/compact/compact/src/index.ts @@ -14,6 +14,7 @@ import type { CompactionResult } from './types.ts' export type { CompactionResult } from './types.ts' export { renderContentBlocks, renderTranscript } from './render.ts' +export { toolPairingBalancedAfter, toolPairingBalancedBefore } from './tool-pairing.ts' /** Minimal agent context compaction needs without depending on the agent package. */ export interface CompactAgentContext { @@ -67,6 +68,8 @@ export abstract class CompactService extends Service { * balanced so assistant tool calls remain paired with their results. A model- * backed implementation forwards cancellation and rejects active, missing, * reversed, or unbalanced ranges. + * Use {@link toolPairingBalancedBefore} and {@link toolPairingBalancedAfter} + * for the edge checks. * * @param session - session to mutate. * @param start - first surface seq, inclusive. diff --git a/packages/compact/compact/src/tool-pairing.ts b/packages/compact/compact/src/tool-pairing.ts new file mode 100644 index 0000000000..0fc0f68dc8 --- /dev/null +++ b/packages/compact/compact/src/tool-pairing.ts @@ -0,0 +1,132 @@ +/** + * Tool-pairing balance over a session surface. Compaction changes surface + * positions, so safe cuts are derived from tool-call/result content in current + * surface order rather than step markers or linked-list fields supplied by a + * caller. + * @module @deepseek-ai/dsh-compact/tool-pairing + */ + +import type { Session, SessionEvent, SurfaceNode } from '@deepseek-ai/dsh-session' + +/** Incremental balance state for one session surface generation. */ +interface BalanceCache { + /** Surface rewrite generation this state describes. */ + generation: number + /** + * Balance of every surface cut in current order: a surface of N nodes has + * N + 1 cuts, entry `i` being the cut before node `i` and the final entry + * the cut after the surface tail. + */ + cutBalanced: readonly boolean[] + /** Current surface position of each node seq, indexing {@link cutBalanced}. */ + indexBySeq: Map + /** In-progress tool-call count after the processed surface tail. */ + inProgressToolCalls: number +} + +const balanceCacheBySession = new WeakMap() + +/** Return how one surface event changes the in-progress tool-call count. */ +function nodeDelta(event: SessionEvent): number { + switch (event.type) { + case 'assistant/message': + return event.data.content.filter(block => block.type === 'tool-call').length + case 'tool/result': + return -1 + default: + return 0 + } +} + +/** Read and validate the event named by a surface node. */ +function eventForNode(events: readonly SessionEvent[], node: SurfaceNode): SessionEvent { + const event = events[node.seq] + if (event === undefined || event.seq !== node.seq) { + throw new Error(`tool-pairing balance: surface seq ${node.seq} has no matching session event (corrupt surface)`) + } + return event +} + +/** Fold surface nodes not yet in the cache into its balance state. */ +function extendCache( + session: Session, + cache: BalanceCache, + nodes: readonly SurfaceNode[], +): BalanceCache { + const processed = cache.cutBalanced.length - 1 + const tail = nodes.slice(processed) + // Validate the unseen tail before mutating the live cache, so a corrupt + // append cannot leave a partially advanced state behind. + const events = session.events + const pendingCuts: boolean[] = [] + let inProgressToolCalls = cache.inProgressToolCalls + for (const node of tail) { + inProgressToolCalls += nodeDelta(eventForNode(events, node)) + if (inProgressToolCalls < 0) { + throw new Error(`tool-pairing balance: tool/result at surface seq ${node.seq} has no matching tool-call (corrupt surface)`) + } + pendingCuts.push(inProgressToolCalls === 0) + } + + tail.forEach((node, offset) => cache.indexBySeq.set(node.seq, processed + offset)) + cache.cutBalanced = cache.cutBalanced.concat(pendingCuts) + cache.inProgressToolCalls = inProgressToolCalls + return cache +} + +/** Return balance state synchronized with the current session surface. */ +function balanceCache(session: Session): BalanceCache { + const surface = session.surface + const nodes = surface.nodes + const generation = surface.replaceGeneration + const cached = balanceCacheBySession.get(session) + + if (cached === undefined || cached.generation !== generation || cached.cutBalanced.length - 1 > nodes.length) { + // A rebuild is the same fold started from the empty-surface state, whose + // single leading cut is trivially balanced. + const rebuilt = extendCache(session, { + generation, + cutBalanced: [true], + indexBySeq: new Map(), + inProgressToolCalls: 0, + }, nodes) + balanceCacheBySession.set(session, rebuilt) + return rebuilt + } + if (cached.cutBalanced.length - 1 < nodes.length) return extendCache(session, cached, nodes) + return cached +} + +/** Balance of the cut at a node's position plus offset, rejecting seqs outside current membership. */ +function cutBalance(cache: BalanceCache, seq: number, offset: 0 | 1): boolean { + const index = cache.indexBySeq.get(seq) + const balanced = index === undefined ? undefined : cache.cutBalanced[index + offset] + if (balanced === undefined) { + throw new Error(`tool-pairing balance: surface seq ${seq} not found`) + } + return balanced +} + +/** + * Whether the cut immediately before a current surface node is tool-pairing balanced. + * @param session - session whose surface is checked. + * @param node - surface node whose leading cut is checked; only its seq identifies it. + * @returns true when no unanswered tool call crosses the cut. + * @throws when the seq is absent from the current surface, a surface node has no + * matching log event, or a tool result has no preceding open call. + */ +export function toolPairingBalancedBefore(session: Session, node: SurfaceNode): boolean { + return cutBalance(balanceCache(session), node.seq, 0) +} + +/** + * Whether the cut immediately after a current surface node is tool-pairing balanced. + * @param session - session whose surface is checked. + * @param node - surface node whose trailing cut is checked; only its seq identifies it. + * @returns true when no unanswered tool call crosses the cut. + * @throws when the seq is absent from the current surface, a surface node has no + * matching log event, or a tool result has no preceding open call. + */ +export function toolPairingBalancedAfter(session: Session, node: SurfaceNode): boolean { + return cutBalance(balanceCache(session), node.seq, 1) +} diff --git a/packages/compact/compact/tests/tool-pairing.spec.ts b/packages/compact/compact/tests/tool-pairing.spec.ts new file mode 100644 index 0000000000..14bb00488a --- /dev/null +++ b/packages/compact/compact/tests/tool-pairing.spec.ts @@ -0,0 +1,330 @@ +import { describe, expect, it } from 'vitest' +import { CallId } from '@deepseek-ai/dsh-llm' +import { toolPairingBalancedAfter, toolPairingBalancedBefore } from '@deepseek-ai/dsh-compact' +import { Session, SessionId } from '@deepseek-ai/dsh-session' +import type { SessionEvent, SurfaceNode } from '@deepseek-ai/dsh-session' + +const SURFACE = { surfaceOp: 'append' as const } + +function seqOf(session: Session, type: SessionEvent['type'], nth = 0): number { + return session.events.filter(event => event.type === type)[nth]!.seq +} + +function nodeAt(session: Session, seq: number): SurfaceNode { + const node = session.surface.nodes.find(candidate => candidate.seq === seq) + if (node === undefined) throw new Error(`seq ${seq} is not a surface node`) + return node +} + +function before(session: Session, type: SessionEvent['type'], nth = 0): boolean { + return toolPairingBalancedBefore(session, nodeAt(session, seqOf(session, type, nth))) +} + +function after(session: Session, type: SessionEvent['type'], nth = 0): boolean { + return toolPairingBalancedAfter(session, nodeAt(session, seqOf(session, type, nth))) +} + +function closedToolStep(): Session { + const session = new Session(SessionId('closed-tool-step')) + session.append('user/message', { + content: [{ type: 'text', text: 'go' }], + source: { kind: 'user' }, + }, SURFACE) + session.append('assistant/message', { + turn: 1, + step: 1, + content: [{ type: 'tool-call', id: CallId('c1'), name: 'bash', arguments: '{}' }], + }, SURFACE) + session.append('tool/result', { + turn: 1, + step: 1, + callId: CallId('c1'), + content: [{ type: 'text', text: 'done' }], + isError: false, + }, SURFACE) + return session +} + +describe('tool-pairing boundaries', () => { + it('classifies closed and open single-call steps', () => { + const closed = closedToolStep() + expect(before(closed, 'user/message')).toBe(true) + expect(after(closed, 'user/message')).toBe(true) + expect(before(closed, 'assistant/message')).toBe(true) + expect(after(closed, 'assistant/message')).toBe(false) + expect(before(closed, 'tool/result')).toBe(false) + expect(after(closed, 'tool/result')).toBe(true) + + const open = new Session(SessionId('open-tool-step')) + open.append('assistant/message', { + turn: 1, + step: 1, + content: [{ type: 'tool-call', id: CallId('open'), name: 'bash', arguments: '{}' }], + }, SURFACE) + expect(toolPairingBalancedAfter(open, open.surface.nodes[0]!)).toBe(false) + }) + + it('requires every result from a multiple-call assistant message', () => { + const session = new Session(SessionId('multiple-calls')) + session.append('assistant/message', { + turn: 1, + step: 1, + content: [ + { type: 'tool-call', id: CallId('c1'), name: 'one', arguments: '{}' }, + { type: 'tool-call', id: CallId('c2'), name: 'two', arguments: '{}' }, + ], + }, SURFACE) + session.append('tool/result', { + turn: 1, step: 1, callId: CallId('c1'), content: [], isError: false, + }, SURFACE) + session.append('tool/result', { + turn: 1, step: 1, callId: CallId('c2'), content: [], isError: false, + }, SURFACE) + + expect(after(session, 'tool/result', 0)).toBe(false) + expect(after(session, 'tool/result', 1)).toBe(true) + }) + + it('keeps neutral nodes inside an open pair unbalanced and free nodes balanced', () => { + const midStep = new Session(SessionId('neutral-mid-step')) + midStep.append('assistant/message', { + turn: 1, + step: 1, + content: [{ type: 'tool-call', id: CallId('c1'), name: 'bash', arguments: '{}' }], + }, SURFACE) + midStep.append('context/message', { + content: [{ type: 'text', text: 'background update' }], + source: { kind: 'plugin', plugin: 'test' }, + }, SURFACE) + midStep.append('tool/result', { + turn: 1, step: 1, callId: CallId('c1'), content: [], isError: false, + }, SURFACE) + expect(before(midStep, 'context/message')).toBe(false) + expect(after(midStep, 'context/message')).toBe(false) + + const free = new Session(SessionId('neutral-free')) + free.append('context/message', { + content: [{ type: 'text', text: 'idle injection' }], + source: { kind: 'user' }, + }, SURFACE) + expect(before(free, 'context/message')).toBe(true) + expect(after(free, 'context/message')).toBe(true) + }) +}) + +describe('tool-pairing surface identity', () => { + it('rebuilds after replace and rejects nodes removed from current membership', () => { + const session = closedToolStep() + const staleTail = nodeAt(session, seqOf(session, 'tool/result')) + expect(toolPairingBalancedAfter(session, staleTail)).toBe(true) + + const nodes = session.surface.nodes + session.append('user/message', { + content: [{ type: 'text', text: 'checkpoint' }], + source: { kind: 'plugin', plugin: 'compact' }, + }, { + surfaceOp: { op: 'replace', start: nodes[0]!.seq, end: nodes.at(-1)!.seq }, + sourceEventSeqs: nodes.map(node => node.seq), + }) + + const checkpoint = session.surface.nodes[0]! + expect(toolPairingBalancedBefore(session, checkpoint)).toBe(true) + expect(toolPairingBalancedAfter(session, checkpoint)).toBe(true) + expect(() => toolPairingBalancedBefore(session, staleTail)).toThrow(/surface seq .* not found/) + expect(() => toolPairingBalancedAfter(session, staleTail)).toThrow(/surface seq .* not found/) + }) + + it('ignores a caller-held node next field and answers from cached balances', () => { + const session = closedToolStep() + const assistant = nodeAt(session, seqOf(session, 'assistant/message')) + expect(toolPairingBalancedAfter(session, { ...assistant, next: null })).toBe(false) + expect(toolPairingBalancedAfter(session, { ...assistant, next: 999 })).toBe(false) + }) + + it('rejects missing seqs before and after, including an empty surface', () => { + const session = new Session(SessionId('missing-membership')) + const missing: SurfaceNode = { seq: 999, prev: null, next: null } + expect(() => toolPairingBalancedBefore(session, missing)).toThrow(/surface seq 999 not found/) + expect(() => toolPairingBalancedAfter(session, missing)).toThrow(/surface seq 999 not found/) + + session.append('user/message', { + content: [{ type: 'text', text: 'first node after empty cache' }], + source: { kind: 'user' }, + }, SURFACE) + expect(toolPairingBalancedAfter(session, session.surface.nodes[0]!)).toBe(true) + }) +}) + +describe('tool-pairing cache refresh', () => { + it('does no event reads for unchanged or log-only growth, folds only appended nodes, and rebuilds on replace', () => { + const events: SessionEvent[] = [ + { + type: 'user/message', seq: 0, time: 0, + data: { content: [{ type: 'text', text: 'user' }], source: { kind: 'user' } }, + surfaceOp: 'append', + }, + { + type: 'assistant/message', seq: 1, time: 1, + data: { turn: 1, step: 1, content: [{ type: 'tool-call', id: CallId('c1'), name: 'one', arguments: '{}' }] }, + surfaceOp: 'append', + }, + { + type: 'tool/result', seq: 2, time: 2, + data: { turn: 1, step: 1, callId: CallId('c1'), content: [], isError: false }, + surfaceOp: 'append', + }, + ] + const nodes: SurfaceNode[] = [ + { seq: 0, prev: null, next: 1 }, + { seq: 1, prev: 0, next: 2 }, + { seq: 2, prev: 1, next: null }, + ] + let generation = 0 + let eventCollectionReads = 0 + let eventIndexReads = 0 + const trackedEvents = new Proxy(events, { + get(target, property, receiver) { + if (typeof property === 'string' && /^\d+$/.test(property)) eventIndexReads += 1 + return Reflect.get(target, property, receiver) as unknown + }, + }) + const surface = { + get nodes() { return nodes }, + get replaceGeneration() { return generation }, + } + const session = { + surface, + get events() { + eventCollectionReads += 1 + return trackedEvents + }, + } as unknown as Session + + expect(toolPairingBalancedAfter(session, nodes[2]!)).toBe(true) + expect(eventCollectionReads).toBe(1) + expect(eventIndexReads).toBe(3) + + expect(toolPairingBalancedBefore(session, nodes[0]!)).toBe(true) + expect(toolPairingBalancedAfter(session, nodes[1]!)).toBe(false) + expect(eventCollectionReads).toBe(1) + expect(eventIndexReads).toBe(3) + + events.push({ + type: 'turn/end', seq: 3, time: 3, + data: { turn: 1, reason: { kind: 'completed' } }, + }) + expect(toolPairingBalancedAfter(session, nodes[2]!)).toBe(true) + expect(eventCollectionReads).toBe(1) + expect(eventIndexReads).toBe(3) + + events.push({ + type: 'user/message', seq: 4, time: 4, + data: { content: [{ type: 'text', text: 'tail' }], source: { kind: 'user' } }, + surfaceOp: 'append', + }) + nodes.push({ seq: 4, prev: 2, next: null }) + expect(toolPairingBalancedAfter(session, nodes[3]!)).toBe(true) + expect(eventCollectionReads).toBe(2) + expect(eventIndexReads).toBe(4) + + events.push( + { + type: 'assistant/message', seq: 5, time: 5, + data: { turn: 2, step: 1, content: [{ type: 'tool-call', id: CallId('c2'), name: 'two', arguments: '{}' }] }, + surfaceOp: 'append', + }, + { + type: 'tool/result', seq: 6, time: 6, + data: { turn: 2, step: 1, callId: CallId('c2'), content: [], isError: false }, + surfaceOp: 'append', + }, + ) + nodes.push( + { seq: 5, prev: 4, next: 6 }, + { seq: 6, prev: 5, next: null }, + ) + expect(toolPairingBalancedAfter(session, nodes[5]!)).toBe(true) + expect(eventCollectionReads).toBe(3) + expect(eventIndexReads).toBe(6) + + events.push({ + type: 'user/message', seq: 7, time: 7, + data: { content: [{ type: 'text', text: 'replacement' }], source: { kind: 'user' } }, + surfaceOp: { op: 'replace', start: 0, end: 6 }, + }) + nodes.splice(0, nodes.length, { seq: 7, prev: null, next: null }) + generation += 1 + expect(toolPairingBalancedAfter(session, nodes[0]!)).toBe(true) + expect(eventCollectionReads).toBe(4) + expect(eventIndexReads).toBe(7) + }) + + it('rebuilds defensively when a same-generation surface node count regresses', () => { + const events: SessionEvent[] = [ + { + type: 'user/message', seq: 0, time: 0, + data: { content: [], source: { kind: 'user' } }, surfaceOp: 'append', + }, + { + type: 'user/message', seq: 1, time: 1, + data: { content: [], source: { kind: 'user' } }, surfaceOp: 'append', + }, + ] + const nodes: SurfaceNode[] = [ + { seq: 0, prev: null, next: 1 }, + { seq: 1, prev: 0, next: null }, + ] + const session = { + events, + surface: { nodes, replaceGeneration: 0 }, + } as unknown as Session + expect(toolPairingBalancedAfter(session, nodes[1]!)).toBe(true) + nodes.pop() + expect(toolPairingBalancedAfter(session, nodes[0]!)).toBe(true) + }) +}) + +describe('tool-pairing corrupt surfaces', () => { + it('throws for an orphan result during a rebuild', () => { + const session = new Session(SessionId('orphan-rebuild')) + session.append('tool/result', { + turn: 1, step: 1, callId: CallId('orphan'), content: [], isError: false, + }, SURFACE) + expect(() => toolPairingBalancedAfter(session, session.surface.nodes[0]!)).toThrow(/no matching tool-call/) + }) + + it('retries an orphan result in an appended tail without committing partial cache state', () => { + const session = new Session(SessionId('orphan-tail')) + session.append('user/message', { + content: [{ type: 'text', text: 'safe head' }], source: { kind: 'user' }, + }, SURFACE) + expect(toolPairingBalancedAfter(session, session.surface.nodes[0]!)).toBe(true) + session.append('tool/result', { + turn: 1, step: 1, callId: CallId('orphan'), content: [], isError: false, + }, SURFACE) + expect(() => toolPairingBalancedAfter(session, session.surface.nodes[1]!)).toThrow(/no matching tool-call/) + expect(() => toolPairingBalancedAfter(session, session.surface.nodes[1]!)).toThrow(/no matching tool-call/) + }) + + it('throws when a current surface seq has no matching event or indexes the wrong event', () => { + const missingNode: SurfaceNode = { seq: 1, prev: null, next: null } + const missing = { + events: [{ + type: 'user/message', seq: 0, time: 0, + data: { content: [], source: { kind: 'user' } }, surfaceOp: 'append', + } satisfies SessionEvent], + surface: { nodes: [missingNode], replaceGeneration: 0 }, + } as unknown as Session + expect(() => toolPairingBalancedBefore(missing, missingNode)).toThrow(/no matching session event/) + + const mismatchedNode: SurfaceNode = { seq: 0, prev: null, next: null } + const mismatched = { + events: [{ + type: 'user/message', seq: 99, time: 0, + data: { content: [], source: { kind: 'user' } }, surfaceOp: 'append', + } satisfies SessionEvent], + surface: { nodes: [mismatchedNode], replaceGeneration: 0 }, + } as unknown as Session + expect(() => toolPairingBalancedBefore(mismatched, mismatchedNode)).toThrow(/no matching session event/) + }) +}) diff --git a/packages/core/session/README.md b/packages/core/session/README.md index 2d28b28a3d..4e585d3cb6 100644 --- a/packages/core/session/README.md +++ b/packages/core/session/README.md @@ -79,7 +79,7 @@ Every `SessionEvent` carries two optional top-level fields (structural metadata) - Persistence plugins: subscribe to `session/event` (write-behind) and drain on `session/flush` (awaited) and fiber dispose. A durable backend reads the log and reloads it into a live session; the metadata seam (`SessionHeader`, `session.header`) is what such a backend stores beside the log. - Replay/fork: `create(id, { seed })` validates and freezes a contiguous log and rebuilds its surface. `fork(source, boundary?, childSessionId?)` selects a completed-turn prefix and records lineage. -- Compaction: the `dsh-compact-basic` plugin appends a `user/message` with `surfaceOp: { op: 'replace', start, end }` to shadow old surface nodes behind a summary checkpoint. +- Compaction: the `dsh-compact-basic` plugin appends a `user/message` with `surfaceOp: { op: 'replace', start, end }` to shadow old surface nodes behind a summary checkpoint. Tool-pairing boundary policy and its cache belong to the [`dsh-compact` seam](../../compact/compact/README.md), while this package owns surface membership, positional links, and `replaceGeneration`. ## Model Experience diff --git a/packages/core/session/src/index.ts b/packages/core/session/src/index.ts index 9b3312c5e9..ff0f8556ab 100644 --- a/packages/core/session/src/index.ts +++ b/packages/core/session/src/index.ts @@ -24,7 +24,6 @@ export type { JsonValue } from './json.ts' export { interruptedTurnClosers } from './repair.ts' export type { SurfaceFoldReplacement, SurfaceFoldResult, SurfaceNode } from './surface.ts' export { foldSurface, isSurfaceEvent, isSurfaceEligibleType } from './surface.ts' -export { isToolPairingBalanced } from './tool-pairing.ts' export { applyHeaderDelta, canonicalHeader, diffHeader, foldRequestHeader, headerEquals } from './request-header.ts' declare module 'cordis' { diff --git a/packages/core/session/src/tool-pairing.ts b/packages/core/session/src/tool-pairing.ts deleted file mode 100644 index ce9dc639c7..0000000000 --- a/packages/core/session/src/tool-pairing.ts +++ /dev/null @@ -1,56 +0,0 @@ -/** - * Tool-pairing balance over a session surface. Compaction changes surface - * positions, so safe cuts are derived from tool-call/result content on the - * surface rather than step markers in the append-only log. - * @module @deepseek-ai/dsh-session/tool-pairing - */ - -import type { SessionEvent } from './types.ts' -import type { SurfaceNode } from './surface.ts' - -/** - * The tool-pairing delta of a surface node: how it shifts the count of - * unanswered tool calls. An `assistant/message` opens one bracket per - * `tool-call` block; a `tool/result` closes one; every other surface node - * (`user/message`, `context/message`, `steering/message`, a usage-only - * `assistant/message` with no tool-call blocks) is pairing-neutral. - */ -function nodeDelta(event: SessionEvent): number { - switch (event.type) { - case 'assistant/message': - return event.data.content.filter(block => block.type === 'tool-call').length - case 'tool/result': - return -1 - // Non-pairing surface nodes and every non-surface event contribute nothing. - default: - return 0 - } -} - -/** - * Check that a surface cut does not split a tool call from its result. A region - * is safe to collapse only when the cuts before its first node and after its - * last node both return `true`. - * @param nodes - the surface linked list in head→tail order. - * @param events - the session log each node's `seq` indexes into. - * @param beforeSeq - node immediately after the cut; `null` or a seq absent from the surface means after-tail. - * @returns whether every call before the cut has its result before the cut. - * @throws if a result appears without a preceding open call. - */ -export function isToolPairingBalanced( - nodes: readonly SurfaceNode[], - events: readonly SessionEvent[], - beforeSeq: number | null, -): boolean { - let depth = 0 - for (const node of nodes) { - if (node.seq === beforeSeq) return depth === 0 - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - depth += nodeDelta(events[node.seq]!) - if (depth < 0) { - throw new Error(`tool-pairing balance: tool/result at surface seq ${node.seq} has no matching tool-call (corrupt surface)`) - } - } - // A missing cut node means the after-tail boundary. - return depth === 0 -} diff --git a/packages/core/session/tests/tool-pairing.spec.ts b/packages/core/session/tests/tool-pairing.spec.ts deleted file mode 100644 index db0f2d26f2..0000000000 --- a/packages/core/session/tests/tool-pairing.spec.ts +++ /dev/null @@ -1,293 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { CallId } from '@deepseek-ai/dsh-llm' -import { Session, SessionId, isToolPairingBalanced } from '../src/index.ts' -import type { SessionEvent, SurfaceNode } from '../src/index.ts' - -/** - * Unit coverage for compaction-cut safety: a cut is balanced only when it - * separates no assistant tool call from its result. Non-step nodes are neutral, - * and replace operations prove surface order—not raw log order—is authoritative. - */ - -const SURFACE = { surfaceOp: 'append' as const } - -/** Surface nodes + log for a session, the two args the balance check takes. */ -function surfaceOf(session: Session): { nodes: readonly SurfaceNode[]; events: readonly SessionEvent[] } { - return { nodes: session.surface.nodes, events: session.events } -} - -/** The cut BEFORE the surface node at `seq` is balanced (safe region start). */ -function startBalanced(session: Session, seq: number): boolean { - const { nodes, events } = surfaceOf(session) - return isToolPairingBalanced(nodes, events, seq) -} - -/** The cut AFTER the surface node at `seq` is balanced (safe region end). */ -function endBalanced(session: Session, seq: number): boolean { - const { nodes, events } = surfaceOf(session) - const node = nodes.find(n => n.seq === seq) - if (!node) throw new Error(`seq ${seq} is not a surface node`) - return isToolPairingBalanced(nodes, events, node.next) -} - -/** Surface seq of the nth (0-based) event of a given type. */ -function seqOf(s: Session, type: SessionEvent['type'], nth = 0): number { - return s.events.filter(e => e.type === type)[nth]!.seq -} - -/** A closed turn with one closed step holding an assistant + its tool result. */ -function toolStepSession(): Session { - const s = new Session(SessionId('tool-step')) - s.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } }) - s.append('user/message', { content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }, SURFACE) - s.append('step/start', { turn: 1, step: 1 }) - s.append('assistant/message', { - turn: 1, step: 1, - content: [ - { type: 'text', text: 'calling' }, - { type: 'tool-call', id: CallId('c1'), name: 'bash', arguments: '{}' }, - ], - }, SURFACE) - s.append('tool/call', { turn: 1, step: 1, callId: CallId('c1'), name: 'bash', arguments: '{}' }) - s.append('tool/result', { turn: 1, step: 1, callId: CallId('c1'), content: [{ type: 'text', text: 'out' }], isError: false }, SURFACE) - s.append('step/end', { turn: 1, step: 1 }) - s.append('turn/end', { turn: 1, reason: { kind: 'completed' } }) - return s -} - -describe('isToolPairingBalanced — region START (cut before a node)', () => { - it('is true for a pre-step user/message (belongs to no step)', () => { - const s = toolStepSession() - expect(startBalanced(s, seqOf(s, 'user/message'))).toBe(true) - }) - - it('is true for the first surface node of a step (the assistant/message)', () => { - // The cut before the assistant is balanced — nothing unanswered precedes it. - const s = toolStepSession() - expect(startBalanced(s, seqOf(s, 'assistant/message'))).toBe(true) - }) - - it('is false for a tool/result whose assistant/message precedes it in the same step', () => { - // The cut before the tool/result has one unanswered tool-call (the - // assistant's) → starting the region here would orphan that call. - const s = toolStepSession() - expect(startBalanced(s, seqOf(s, 'tool/result'))).toBe(false) - }) - - it('is true at the surface head (nothing precedes)', () => { - const s = new Session(SessionId('lone')) - s.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } }) - s.append('user/message', { content: [{ type: 'text', text: 'hi' }], source: { kind: 'user' } }, SURFACE) - expect(startBalanced(s, seqOf(s, 'user/message'))).toBe(true) - }) -}) - -describe('isToolPairingBalanced — region END (cut after a node)', () => { - it('is true for the last surface node of a closed step (the tool/result)', () => { - // After the tool/result the assistant's single call is answered → balanced. - const s = toolStepSession() - expect(endBalanced(s, seqOf(s, 'tool/result'))).toBe(true) - }) - - it('is false for an assistant/message with a later tool/result in the same step', () => { - // After the assistant its tool-call is still unanswered → ending here strands - // the result. - const s = toolStepSession() - expect(endBalanced(s, seqOf(s, 'assistant/message'))).toBe(false) - }) - - it('is true for a pre-step user/message', () => { - const s = toolStepSession() - expect(endBalanced(s, seqOf(s, 'user/message'))).toBe(true) - }) - - it('is false at the tail when the node is inside an open (unclosed) step', () => { - // step/start then an assistant tool-call, but no tool/result yet (mid-flight). - // The after-tail cut still has one unanswered call → not balanced. - const s = new Session(SessionId('open-step')) - s.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } }) - s.append('step/start', { turn: 1, step: 1 }) - s.append('assistant/message', { - turn: 1, step: 1, - content: [{ type: 'tool-call', id: CallId('c1'), name: 'bash', arguments: '{}' }], - }, SURFACE) - expect(endBalanced(s, seqOf(s, 'assistant/message'))).toBe(false) - }) - - it('is true at the tail when the node is a trailing inter-step node (step already closed)', () => { - // A steering message appended after step/end, at the tail. The prior step's - // pair is balanced and steering is neutral → the after-tail cut is balanced. - const s = new Session(SessionId('trailing-steer')) - s.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } }) - s.append('step/start', { turn: 1, step: 1 }) - s.append('assistant/message', { turn: 1, step: 1, content: [{ type: 'text', text: 'a' }] }, SURFACE) - s.append('step/end', { turn: 1, step: 1 }) - s.append('steering/message', { turn: 1, content: [{ type: 'text', text: 's' }], source: { kind: 'user' } }, SURFACE) - expect(endBalanced(s, seqOf(s, 'steering/message'))).toBe(true) - }) - - it('is true at the tail when no step ever opened', () => { - const s = new Session(SessionId('no-step')) - s.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } }) - s.append('user/message', { content: [{ type: 'text', text: 'hi' }], source: { kind: 'user' } }, SURFACE) - expect(endBalanced(s, seqOf(s, 'user/message'))).toBe(true) - }) -}) - -describe('isToolPairingBalanced — multiple tool calls in one assistant message', () => { - // An assistant message with two tool-calls needs BOTH results before the cut - // after it is balanced — depth +2, then -1, -1. - function twoCallStep(): Session { - const s = new Session(SessionId('two-call')) - s.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } }) - s.append('step/start', { turn: 1, step: 1 }) - s.append('assistant/message', { - turn: 1, step: 1, - content: [ - { type: 'tool-call', id: CallId('c1'), name: 'a', arguments: '{}' }, - { type: 'tool-call', id: CallId('c2'), name: 'b', arguments: '{}' }, - ], - }, SURFACE) - s.append('tool/result', { turn: 1, step: 1, callId: CallId('c1'), content: [{ type: 'text', text: '1' }], isError: false }, SURFACE) - s.append('tool/result', { turn: 1, step: 1, callId: CallId('c2'), content: [{ type: 'text', text: '2' }], isError: false }, SURFACE) - s.append('step/end', { turn: 1, step: 1 }) - s.append('turn/end', { turn: 1, reason: { kind: 'completed' } }) - return s - } - - it('is unbalanced after the first of two results (one call still open)', () => { - const s = twoCallStep() - expect(endBalanced(s, seqOf(s, 'tool/result', 0))).toBe(false) - }) - - it('is balanced after the second result (both calls answered)', () => { - const s = twoCallStep() - expect(endBalanced(s, seqOf(s, 'tool/result', 1))).toBe(true) - }) -}) - -describe('isToolPairingBalanced — a mid-step injection context/message', () => { - // The injected context is pairing-neutral, but both adjacent cuts remain - // unbalanced because the tool call is still open across them. - function midStepInjection(): Session { - const s = new Session(SessionId('mid-inject')) - s.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } }) - s.append('step/start', { turn: 1, step: 1 }) - s.append('assistant/message', { - turn: 1, step: 1, - content: [{ type: 'tool-call', id: CallId('c1'), name: 'bash', arguments: '{}' }], - }, SURFACE) - s.append('context/message', { content: [{ type: 'text', text: 'bg task done' }], source: { kind: 'plugin', plugin: 'tool-bash' } }, SURFACE) - s.append('tool/result', { turn: 1, step: 1, callId: CallId('c1'), content: [{ type: 'text', text: 'out' }], isError: false }, SURFACE) - s.append('step/end', { turn: 1, step: 1 }) - s.append('turn/end', { turn: 1, reason: { kind: 'completed' } }) - return s - } - - it('start cut before the mid-step context/message is unbalanced (call still open)', () => { - const s = midStepInjection() - expect(startBalanced(s, seqOf(s, 'context/message'))).toBe(false) - }) - - it('end cut after the mid-step context/message is unbalanced (call still open)', () => { - const s = midStepInjection() - expect(endBalanced(s, seqOf(s, 'context/message'))).toBe(false) - }) -}) - -describe('isToolPairingBalanced on an injection turn (no step)', () => { - // An idle inject() wraps a context/message in a bare turn/start → - // context/message → turn/end with NO step. The context node is a free boundary - // both ways (pairing-neutral, nothing open around it). - function injectionSession(): Session { - const s = new Session(SessionId('injection')) - s.append('turn/start', { turn: 1, trigger: { kind: 'injection', source: { kind: 'user' } } }) - s.append('context/message', { content: [{ type: 'text', text: 'ctx' }], source: { kind: 'user' } }, SURFACE) - s.append('turn/end', { turn: 1, reason: { kind: 'completed' } }) - return s - } - - it('start: balanced', () => { - const s = injectionSession() - expect(startBalanced(s, seqOf(s, 'context/message'))).toBe(true) - }) - - it('end: balanced', () => { - const s = injectionSession() - expect(endBalanced(s, seqOf(s, 'context/message'))).toBe(true) - }) -}) - -describe('isToolPairingBalanced — CBR-001: a head checkpoint left by a replace op', () => { - // A replacement checkpoint has a high log seq but sits at the surface head; - // its cuts are balanced regardless of later raw-log neighbors. - function checkpointHeadedSession(): Session { - const s = new Session(SessionId('checkpoint')) - // A closed turn with a tool step → surface [u1, asst(call), result]. - s.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } }) - s.append('step/start', { turn: 1, step: 1 }) - s.append('user/message', { content: [{ type: 'text', text: 'u1' }], source: { kind: 'user' } }, SURFACE) - s.append('assistant/message', { - turn: 1, step: 1, - content: [{ type: 'tool-call', id: CallId('c1'), name: 'bash', arguments: '{}' }], - }, SURFACE) - s.append('tool/result', { turn: 1, step: 1, callId: CallId('c1'), content: [{ type: 'text', text: 'out' }], isError: false }, SURFACE) - s.append('step/end', { turn: 1, step: 1 }) - s.append('turn/end', { turn: 1, reason: { kind: 'completed' } }) - // An OPEN turn whose step is in progress (loop fires compaction here). - s.append('turn/start', { turn: 2, trigger: { kind: 'message', source: { kind: 'user' } } }) - s.append('step/start', { turn: 2, step: 1 }) - // Compaction replaces the whole turn-1 surface ([u1, asst, result]) with one - // summary user/message — appended now, so it carries a high log seq. - const u1 = seqOf(s, 'user/message') - const result = s.events.find(e => e.type === 'tool/result')!.seq - const shadowedSeqs = s.surface.nodes.map(node => node.seq) - s.append('user/message', { - content: [{ type: 'text', text: 'CHECKPOINT' }], - source: { kind: 'plugin', plugin: 'compact' }, - }, { surfaceOp: { op: 'replace', start: u1, end: result }, sourceEventSeqs: shadowedSeqs }) - // The step's own assistant/message lands AFTER the checkpoint in the log, - // still inside the open step. - s.append('assistant/message', { turn: 2, step: 1, content: [{ type: 'text', text: 'a2' }] }, SURFACE) - return s - } - - it('the head checkpoint sits at the surface head while a later surface node follows it in the log', () => { - const s = checkpointHeadedSession() - const nodes = s.surface.nodes - const checkpointSeq = nodes[0]!.seq - // The checkpoint heads the surface, yet a surface node (the open step's - // assistant) follows it in LOG order — the exact split between surface - // position and log position that the log-position scan tripped on. - const laterSurfaceInLog = s.events.find( - e => e.seq > checkpointSeq && nodes.some(n => n.seq === e.seq), - ) - expect(laterSurfaceInLog).toBeDefined() - expect(nodes[0]!.seq).toBe(checkpointSeq) - }) - - it('start cut before the head checkpoint is balanced (it is the head)', () => { - const s = checkpointHeadedSession() - expect(startBalanced(s, s.surface.nodes[0]!.seq)).toBe(true) - }) - - it('end cut after the head checkpoint is balanced (it carries no tool pair)', () => { - // This is the exact assertion the log-position scan failed: the forward log scan from the - // checkpoint reached the open step's assistant/message and wrongly reported mid-step. - const s = checkpointHeadedSession() - expect(endBalanced(s, s.surface.nodes[0]!.seq)).toBe(true) - }) -}) - -describe('isToolPairingBalanced — corrupt surface guard', () => { - it('throws when a tool/result has no preceding tool-call (depth goes negative)', () => { - // A surface that opens with a tool/result (no assistant call before it) is - // structurally corrupt — surfaced loudly rather than mis-classified. - const s = new Session(SessionId('corrupt')) - s.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } }) - s.append('step/start', { turn: 1, step: 1 }) - s.append('tool/result', { turn: 1, step: 1, callId: CallId('c1'), content: [{ type: 'text', text: 'x' }], isError: false }, SURFACE) - const { nodes, events } = surfaceOf(s) - expect(() => isToolPairingBalanced(nodes, events, null)).toThrow(/no matching tool-call/) - }) -}) diff --git a/packages/session-persistence/session-persistence/README.md b/packages/session-persistence/session-persistence/README.md index a294575fd9..3390b2cc30 100644 --- a/packages/session-persistence/session-persistence/README.md +++ b/packages/session-persistence/session-persistence/README.md @@ -25,6 +25,8 @@ The persisted unit IS the existing `SessionEvent` (event-sourced model — the l `PersistenceCoordinator` owns per-id state, write-behind buffers and serialization, the `session/event` → `session/flush` drain, lazy materialization, crash-tail repair, session adoption, and quiescent disposal. A first-party backend composes one, implements the small `PersistenceBackend` storage hook interface, and delegates its four public service methods. JSONL and SQLite therefore share lifecycle correctness while retaining different storage primitives; see the [coordinator RFC](../../../docs/rfc/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md). +When a live session emits `session/disposed`, the coordinator waits for its initialization, serializes a final buffer drain, then releases every map entry owned by that exact `Session` object. A failed final drain keeps the pending buffer for backend teardown to retry. Backend teardown stops event admission first, awaits all in-flight session retirements and remaining per-id operations, drains any retained buffers, and only then closes the storage handle. + The side-effect-free `locate` query remains backend-owned because it describes storage topology rather than write orchestration. The `PersistenceBackend` hooks (the only seam between the coordinator and storage): diff --git a/packages/session-persistence/session-persistence/src/coordinator.ts b/packages/session-persistence/session-persistence/src/coordinator.ts index 4a2fed8d34..686b9790d4 100644 --- a/packages/session-persistence/session-persistence/src/coordinator.ts +++ b/packages/session-persistence/session-persistence/src/coordinator.ts @@ -126,7 +126,8 @@ function seedCoversPrefix(seed: readonly SessionEvent[], prefix: readonly Sessio * * All per-id operations are serialized (a per-id promise chain) so concurrent * flushes / a flush racing a load never interleave storage writes. The - * constructor installs the write-path listeners and the dispose effect. + * constructor installs the write-path listeners, per-session retirement, and + * the backend dispose effect. * * @typeParam TornMarker - the backend's opaque torn-tail repair token. */ @@ -146,6 +147,8 @@ export class PersistenceCoordinator { * observation boundary; callers do not inspect this bookkeeping directly. */ private inits = new Map>() + /** Final drains started by fire-and-forget session disposal notifications. */ + private retirements = new Set>() constructor(private ctx: Context, private backend: PersistenceBackend) { this.installWritePath() @@ -267,7 +270,13 @@ export class PersistenceCoordinator { const next = prior.then(op, op) // Keep the chain alive but swallow this op's rejection for the NEXT waiter // (the caller still sees the real rejection via `next`). - this.chains.set(id, next.then(() => undefined, () => undefined)) + const tail = next.then(() => undefined, () => undefined) + this.chains.set(id, tail) + // Settled tails carry no serialization value. Delete only the exact tail + // installed above: a later operation may already have replaced it. + void tail.then(() => { + if (this.chains.get(id) === tail) this.chains.delete(id) + }) return next } @@ -293,27 +302,12 @@ export class PersistenceCoordinator { private installWritePath(): void { const ctx = this.ctx - // Capture the header on creation; persist a fork's seed once. Record the init - // promise so flush/dispose can await it (onCreated is async). - ctx.on('session/created', (session) => { void this.initFor(session) }) - - // Session emits an owned frozen event. Keep a persistence-owned copy anyway - // so the write-behind queue owns exactly the record it will flush rather than - // retaining a product-layer record by identity. Serializability is guaranteed - // at the source, so structuredClone is safe. - ctx.on('session/event', (session, event) => { - let buffer = this.buffers.get(session) - if (!buffer) this.buffers.set(session, buffer = []) - buffer.push(structuredClone(event)) - }) - - // Drain to the backend at the durability checkpoint. - ctx.on('session/flush', session => this.flush(session)) - - // Dispose must reach quiescence: await every init + final drain BEFORE - // returning, then close the backend's own resources (AFTER the drain), so no - // write lands after teardown and a close failure never MASKS a drain error. + // Register the disposer BEFORE the listeners. Cordis tears effects down in + // reverse registration order, so event admission closes before this final + // drain reaches quiescence and closes the backend. ctx.effect(() => async () => { + await this.awaitRetirements() + let disposeError: unknown try { const errors = [ @@ -341,11 +335,63 @@ export class PersistenceCoordinator { } }, `${this.backend.name} write path`) + // Capture the header on creation; persist a fork's seed once. Record the init + // promise so flush/dispose can await it (onCreated is async). + ctx.on('session/created', (session) => { void this.initFor(session) }) + + // Session emits an owned frozen event. Keep a persistence-owned copy anyway + // so the write-behind queue owns exactly the record it will flush rather than + // retaining a product-layer record by identity. Serializability is guaranteed + // at the source, so structuredClone is safe. + ctx.on('session/event', (session, event) => { + let buffer = this.buffers.get(session) + if (!buffer) this.buffers.set(session, buffer = []) + buffer.push(structuredClone(event)) + }) + + // Drain to the backend at the durability checkpoint. + ctx.on('session/flush', session => this.flush(session)) + + // Session disposal is observe-only, so the coordinator observes the + // detached task itself and backend teardown awaits quiescence. + ctx.on('session/disposed', (session) => { this.retire(session) }) + // HMR: a hot reload does not replay session/created, so seed existing live // sessions (mirrors dsh-invariants). for (const session of ctx.sessions.list()) void this.initFor(session) } + /** Start, observe, and track one disposed session's final drain. */ + private retire(session: Session): void { + const task = this.retireCore(session) + this.retirements.add(task) + const settled = (): void => { this.retirements.delete(task) } + void task.then(settled, (error: unknown) => { + settled() + this.ctx.logger.warn(`${this.backend.name}: session "${session.id}" retirement failed: ${String(error)}`) + }) + } + + /** Drain and release state owned by one exact disposed Session lifecycle. */ + private async retireCore(session: Session): Promise { + await this.inits.get(session) + + const id = session.header.id + await this.serialize(id, async () => { + await this.drain(session) + this.buffers.delete(session) + this.inits.delete(session) + if (this.states.get(id)?.owner === session) this.states.delete(id) + }) + } + + /** Await every retirement admitted before listener teardown. */ + private async awaitRetirements(): Promise { + while (this.retirements.size > 0) { + await Promise.allSettled([...this.retirements]) + } + } + /** Start (once) the async init for a session and remember its promise. */ private initFor(session: Session): Promise { const existing = this.inits.get(session) diff --git a/packages/session-persistence/session-persistence/tests/coordinator-contract.ts b/packages/session-persistence/session-persistence/tests/coordinator-contract.ts index f6692b9175..defcaefb6d 100644 --- a/packages/session-persistence/session-persistence/tests/coordinator-contract.ts +++ b/packages/session-persistence/session-persistence/tests/coordinator-contract.ts @@ -9,7 +9,7 @@ * @module @deepseek-ai/dsh-session-persistence/tests/coordinator-contract */ -import { describe, expect, it } from 'vitest' +import { describe, expect, it, vi } from 'vitest' import { Context, type Fiber } from 'cordis' import SessionStore, { SESSION_FORMAT_VERSION, SessionId } from '@deepseek-ai/dsh-session' import type { Session, SessionEvent } from '@deepseek-ai/dsh-session' @@ -401,7 +401,7 @@ export function runCoordinatorContract(name: string, makeFixture: () => Promise< } }) - it('does NOT reclaim an id whose abandoned owner still has buffered (unflushed) events', async () => { + it('session disposal drains buffered events before retiring ownership', async () => { const fix = await makeFixture() const { ctx, fiber } = await freshCtx(fix) try { @@ -413,13 +413,20 @@ export function runCoordinatorContract(name: string, makeFixture: () => Promise< // Append a turn but do NOT flush — events sit in the write-behind buffer. first.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } }) first.append('turn/end', { turn: 1, reason: { kind: 'completed' } }) - await firstFiber.dispose() // disposed before flush; not materialized, buffer pending + await firstFiber.dispose() + + // Disposal is an observe-only notification. Poll storage rather than + // assuming the owning fiber awaits the coordinator's detached drain. + await vi.waitFor(async () => { + expect((await ctx.sessionPersistence.list()).map(meta => meta.id)).toContain(SessionId('buffered')) + }) + expect((await ctx.sessionPersistence.load(SessionId('buffered'))).events.map(event => event.seq)).toEqual([0, 1]) let reuse!: Session await ctx.plugin(Object.assign((inner: Context) => { reuse = inner.sessions.create(SessionId('buffered'), { meta: { cwd: WORK } }) }, { inject: ['sessions'] })) - await expect(ctx.sessions.flush(reuse)).rejects.toThrow(/already bound to a different live session/) + await expect(ctx.sessions.flush(reuse)).rejects.toThrow(/persisted log|id collision/) } finally { await fiber.dispose() await fix.cleanup() diff --git a/packages/session-persistence/session-persistence/tests/persistence.spec.ts b/packages/session-persistence/session-persistence/tests/persistence.spec.ts index 5a0ec2d3c3..767e323514 100644 --- a/packages/session-persistence/session-persistence/tests/persistence.spec.ts +++ b/packages/session-persistence/session-persistence/tests/persistence.spec.ts @@ -1,7 +1,7 @@ -import { describe, expect, it } from 'vitest' +import { describe, expect, it, vi } from 'vitest' import { Context } from 'cordis' import SessionStore, { SessionId, isJsonValue } from '@deepseek-ai/dsh-session' -import type { SessionEvent, SessionHeader } from '@deepseek-ai/dsh-session' +import type { Session, SessionEvent, SessionHeader } from '@deepseek-ai/dsh-session' import { SessionPersistence, PersistenceCoordinator, type PersistenceBackend, type StoredPrefix, @@ -15,6 +15,15 @@ type MemoryStore = Map /** Optional plugin config: an EXTERNAL store shared across backend instances. */ interface MemoryConfig { store?: MemoryStore } +/** Test-only view of the coordinator containers whose retirement is the contract under test. */ +interface CoordinatorInternals { + states: Map + buffers: Map + chains: Map + inits: Map + retirements: Set> +} + /** * Reference {@link PersistenceCoordinator} vehicle and abstract-service coverage, backed by a * dependency-free map with atomic writes and no torn-tail marker. Supplying the map lets multiple @@ -101,6 +110,49 @@ class MemoryPersistence extends SessionPersistence implements PersistenceBackend } } +/** Controllable storage primitive for serialization and retirement failure tests. */ +class ControlledBackend implements PersistenceBackend { + readonly name = 'session-persistence-controlled' + readonly store: MemoryStore = new Map() + readonly lifecycle: string[] = [] + appendAttempts = 0 + loadAttempts = 0 + beforeAppend?: (attempt: number) => Promise + beforeLoadStored?: (attempt: number) => Promise + + async loadStored(id: SessionId): Promise | undefined> { + await this.beforeLoadStored?.(++this.loadAttempts) + const entry = this.store.get(id) + if (entry === undefined) return undefined + return { meta: structuredClone(entry.meta), events: structuredClone(entry.events) } + } + + loadLive(id: SessionId, _cwd: string | undefined): Promise | undefined> { + return this.loadStored(id) + } + + async appendBatch(m: SessionHeader, events: readonly SessionEvent[], _isMaterialized: boolean): Promise { + const attempt = ++this.appendAttempts + await this.beforeAppend?.(attempt) + const entry = this.store.get(m.id) + if (entry === undefined) { + this.store.set(m.id, { meta: structuredClone(m), events: structuredClone(events) as SessionEvent[] }) + } else { + entry.events.push(...structuredClone(events) as SessionEvent[]) + } + } + + async commitRepair(_m: SessionHeader, _tornMarker: undefined, _closers: readonly SessionEvent[]): Promise {} + + async list(): Promise { + return [...this.store.values()].map(entry => structuredClone(entry.meta)) + } + + async close(): Promise { + this.lifecycle.push('close') + } +} + // Run the shared contract against the in-memory backend. runPersistenceContract('memory', async () => { const ctx = new Context() @@ -122,6 +174,230 @@ runCoordinatorContract('memory', async (): Promise => { } }) +describe('PersistenceCoordinator retirement', () => { + it('a retiring unmaterialized owner without buffered events releases its id', async () => { + const ctx = new Context() + await ctx.plugin(SessionStore) + const backend = new ControlledBackend() + let coordinator!: PersistenceCoordinator + const backendFiber = await ctx.plugin(Object.assign((inner: Context) => { + coordinator = new PersistenceCoordinator(inner, backend) + }, { inject: ['sessions'] })) + const loadGate = Promise.withResolvers() + + try { + const id = SessionId('retiring-lazy-owner') + let first!: Session + const firstFiber = await ctx.plugin(Object.assign((inner: Context) => { + first = inner.sessions.create(id) + }, { inject: ['sessions'] })) + await ctx.sessions.flush(first) + + const baselineLoads = backend.loadAttempts + backend.beforeLoadStored = async () => { await loadGate.promise } + const blockingLoad = coordinator.load(id) + await vi.waitFor(() => { expect(backend.loadAttempts).toBe(baselineLoads + 1) }) + await firstFiber.dispose() + + let reuse!: Session + await ctx.plugin(Object.assign((inner: Context) => { + reuse = inner.sessions.create(id) + }, { inject: ['sessions'] })) + await vi.waitFor(() => { expect(backend.loadAttempts).toBe(baselineLoads + 2) }) + + loadGate.resolve(true) + await expect(blockingLoad).rejects.toThrow(/not found/) + await expect(ctx.sessions.flush(reuse)).resolves.toBeUndefined() + } finally { + loadGate.resolve(true) + await backendFiber.dispose() + await ctx.fiber.dispose() + } + }) + + it('a retiring owner with buffered events still rejects same-id reuse', async () => { + const ctx = new Context() + await ctx.plugin(SessionStore) + const backend = new ControlledBackend() + let coordinator!: PersistenceCoordinator + const backendFiber = await ctx.plugin(Object.assign((inner: Context) => { + coordinator = new PersistenceCoordinator(inner, backend) + }, { inject: ['sessions'] })) + const loadGate = Promise.withResolvers() + + try { + const id = SessionId('retiring-buffered-owner') + let first!: Session + const firstFiber = await ctx.plugin(Object.assign((inner: Context) => { + first = inner.sessions.create(id) + }, { inject: ['sessions'] })) + await ctx.sessions.flush(first) + first.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } }) + first.append('turn/end', { turn: 1, reason: { kind: 'completed' } }) + + const baselineLoads = backend.loadAttempts + backend.beforeLoadStored = async () => { await loadGate.promise } + const blockingLoad = coordinator.load(id) + await vi.waitFor(() => { expect(backend.loadAttempts).toBe(baselineLoads + 1) }) + await firstFiber.dispose() + + let reuse!: Session + await ctx.plugin(Object.assign((inner: Context) => { + reuse = inner.sessions.create(id) + }, { inject: ['sessions'] })) + await expect(ctx.sessions.flush(reuse)).rejects.toThrow(/bound to a different live session/) + + loadGate.resolve(true) + await expect(blockingLoad).rejects.toThrow(/not found/) + await vi.waitFor(() => { + expect(backend.store.get(id)?.events.map(event => event.seq)).toEqual([0, 1]) + }) + } finally { + loadGate.resolve(true) + await backendFiber.dispose() + await ctx.fiber.dispose() + } + }) + + it('a settled chain tail cannot delete a newer operation for the same id', async () => { + const ctx = new Context() + await ctx.plugin(SessionStore) + const backend = new ControlledBackend() + let coordinator!: PersistenceCoordinator + const fiber = await ctx.plugin(Object.assign((inner: Context) => { + coordinator = new PersistenceCoordinator(inner, backend) + }, { inject: ['sessions'] })) + const internals = coordinator as unknown as CoordinatorInternals + const first = Promise.withResolvers() + const second = Promise.withResolvers() + backend.beforeAppend = async (attempt) => { + if (attempt === 1) await first.promise + if (attempt === 2) await second.promise + } + + try { + const id = SessionId('chain-tail') + await coordinator.create(meta(id)) + const firstAppend = coordinator.append(id, [{ + type: 'turn/start', + seq: 0, + time: 1, + data: { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } }, + }]) + const secondAppend = coordinator.append(id, [{ + type: 'turn/end', + seq: 1, + time: 2, + data: { turn: 1, reason: { kind: 'completed' } }, + }]) + + await vi.waitFor(() => { expect(backend.appendAttempts).toBe(1) }) + first.resolve(true) + await vi.waitFor(() => { expect(backend.appendAttempts).toBe(2) }) + expect(internals.chains.size).toBe(1) + second.resolve(true) + await Promise.all([firstAppend, secondAppend]) + await vi.waitFor(() => { expect(internals.chains.size).toBe(0) }) + expect(backend.store.get(id)?.events.map(event => event.seq)).toEqual([0, 1]) + } finally { + first.resolve(true) + second.resolve(true) + await fiber.dispose() + await ctx.fiber.dispose() + } + }) + + it('backend teardown retries a failed session retirement before close', async () => { + const ctx = new Context() + await ctx.plugin(SessionStore) + const backend = new ControlledBackend() + let coordinator!: PersistenceCoordinator + const backendFiber = await ctx.plugin(Object.assign((inner: Context) => { + coordinator = new PersistenceCoordinator(inner, backend) + }, { inject: ['sessions'] })) + const internals = coordinator as unknown as CoordinatorInternals + backend.beforeAppend = async (attempt) => { + if (attempt === 1) { + backend.lifecycle.push('append-failed') + throw new Error('transient append failure') + } + backend.lifecycle.push('append-committed') + } + + try { + let session!: Session + const sessionFiber = await ctx.plugin(Object.assign((inner: Context) => { + session = inner.sessions.create(SessionId('retry-retirement')) + }, { inject: ['sessions'] })) + session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } }) + session.append('turn/end', { turn: 1, reason: { kind: 'completed' } }) + await sessionFiber.dispose() + + await vi.waitFor(() => { + expect(backend.appendAttempts).toBe(1) + expect(internals.retirements.size).toBe(0) + }) + expect([...internals.buffers.values()]).toEqual([expect.arrayContaining([ + expect.objectContaining({ seq: 0 }), + expect.objectContaining({ seq: 1 }), + ])]) + + await backendFiber.dispose() + expect(backend.store.get(SessionId('retry-retirement'))?.events.map(event => event.seq)).toEqual([0, 1]) + expect(backend.lifecycle).toEqual(['append-failed', 'append-committed', 'close']) + } finally { + await backendFiber.dispose() + await ctx.fiber.dispose() + } + }) + + it('backend teardown waits for an in-flight session retirement before close', async () => { + const ctx = new Context() + await ctx.plugin(SessionStore) + const backend = new ControlledBackend() + let coordinator!: PersistenceCoordinator + const backendFiber = await ctx.plugin(Object.assign((inner: Context) => { + coordinator = new PersistenceCoordinator(inner, backend) + }, { inject: ['sessions'] })) + const internals = coordinator as unknown as CoordinatorInternals + const appendGate = Promise.withResolvers() + backend.beforeAppend = async () => { + backend.lifecycle.push('append-started') + await appendGate.promise + backend.lifecycle.push('append-committed') + } + + try { + let session!: Session + const sessionFiber = await ctx.plugin(Object.assign((inner: Context) => { + session = inner.sessions.create(SessionId('inflight-retirement')) + }, { inject: ['sessions'] })) + session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } }) + session.append('turn/end', { turn: 1, reason: { kind: 'completed' } }) + await sessionFiber.dispose() + await vi.waitFor(() => { + expect(backend.appendAttempts).toBe(1) + expect(internals.retirements.size).toBe(1) + }) + + let disposed = false + const teardown = backendFiber.dispose().then(() => { disposed = true }) + await Promise.resolve() + expect(disposed).toBe(false) + expect(backend.lifecycle).toEqual(['append-started']) + + appendGate.resolve(true) + await teardown + expect(backend.store.get(SessionId('inflight-retirement'))?.events.map(event => event.seq)).toEqual([0, 1]) + expect(backend.lifecycle).toEqual(['append-started', 'append-committed', 'close']) + } finally { + appendGate.resolve(true) + await backendFiber.dispose() + await ctx.fiber.dispose() + } + }) +}) + describe('SessionPersistence service registration', () => { it('registers as ctx.sessionPersistence and is removed on fiber dispose (HMR safety)', async () => { const ctx = new Context() @@ -155,4 +431,37 @@ describe('SessionPersistence service registration', () => { .rejects.toThrow('session metadata must be losslessly JSON-serializable') await fiber.dispose() }) + + it('retires all coordinator bookkeeping for disposed sessions', async () => { + const ctx = new Context() + await ctx.plugin(SessionStore) + const fiber = await ctx.plugin(MemoryPersistence) + const { coordinator } = ctx.sessionPersistence as unknown as { coordinator: CoordinatorInternals } + + try { + for (let index = 0; index < 3; index += 1) { + let session!: Session + const sessionFiber = await ctx.plugin(Object.assign((inner: Context) => { + session = inner.sessions.create(SessionId(`disposed-${index}`)) + }, { inject: ['sessions'] })) + session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } }) + session.append('turn/end', { turn: 1, reason: { kind: 'completed' } }) + await ctx.sessions.flush(session) + await sessionFiber.dispose() + } + + await vi.waitFor(() => { + expect(ctx.sessions.list()).toHaveLength(0) + expect({ + states: coordinator.states.size, + buffers: coordinator.buffers.size, + chains: coordinator.chains.size, + inits: coordinator.inits.size, + retirements: coordinator.retirements.size, + }).toEqual({ states: 0, buffers: 0, chains: 0, inits: 0, retirements: 0 }) + }) + } finally { + await fiber.dispose() + } + }) }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 303d4d33ad..b748fa7509 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2559,6 +2559,18 @@ importers: specifier: ^1.8.1 version: 1.8.1 + website: + devDependencies: + markdown-it-mathjax3: + specifier: ^4.3.2 + version: 4.3.2 + vitepress: + specifier: ^1.6.3 + version: 1.6.4(@algolia/client-search@5.55.2)(@types/node@25.9.3)(lightningcss@1.32.0)(markdown-it-mathjax3@4.3.2)(postcss@8.5.15)(search-insights@2.17.3)(typescript@6.0.3) + vue: + specifier: ^3.5.13 + version: 3.5.39(typescript@6.0.3) + packages: '@agentclientprotocol/sdk@0.25.1': @@ -2566,6 +2578,82 @@ packages: peerDependencies: zod: ^3.25.0 || ^4.0.0 + '@algolia/abtesting@1.21.2': + resolution: {integrity: sha512-uXj0rgk30EpsKvOpuS+R+1XFDrnm56hED1Lz56e8uBkZdKCxw99LS2U8eXBqAHYU8kpkbsnV1GC8velBG070Hg==} + engines: {node: '>= 14.0.0'} + + '@algolia/autocomplete-core@1.17.7': + resolution: {integrity: sha512-BjiPOW6ks90UKl7TwMv7oNQMnzU+t/wk9mgIDi6b1tXpUek7MW0lbNOUHpvam9pe3lVCf4xPFT+lK7s+e+fs7Q==} + + '@algolia/autocomplete-plugin-algolia-insights@1.17.7': + resolution: {integrity: sha512-Jca5Ude6yUOuyzjnz57og7Et3aXjbwCSDf/8onLHSQgw1qW3ALl9mrMWaXb5FmPVkV3EtkD2F/+NkT6VHyPu9A==} + peerDependencies: + search-insights: '>= 1 < 3' + + '@algolia/autocomplete-preset-algolia@1.17.7': + resolution: {integrity: sha512-ggOQ950+nwbWROq2MOCIL71RE0DdQZsceqrg32UqnhDz8FlO9rL8ONHNsI2R1MH0tkgVIDKI/D0sMiUchsFdWA==} + peerDependencies: + '@algolia/client-search': '>= 4.9.1 < 6' + algoliasearch: '>= 4.9.1 < 6' + + '@algolia/autocomplete-shared@1.17.7': + resolution: {integrity: sha512-o/1Vurr42U/qskRSuhBH+VKxMvkkUVTLU6WZQr+L5lGZZLYWyhdzWjW0iGXY7EkwRTjBqvN2EsR81yCTGV/kmg==} + peerDependencies: + '@algolia/client-search': '>= 4.9.1 < 6' + algoliasearch: '>= 4.9.1 < 6' + + '@algolia/client-abtesting@5.55.2': + resolution: {integrity: sha512-y7Epol8HcjlBxKXHhyhfFPFhm78B3P6x9cCbCyGTdxjsdVCptXCy5hpkZWxjGpnaLHvWsHS4QRF0TiBOLst2xg==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-analytics@5.55.2': + resolution: {integrity: sha512-8Pxj2VVmpM2d+UZufnlTq7T1QIcYPVugLV5XC50PnHsV5uRM9CSoYkg2Y+CwqwRk2La0xK5QsfZ0obIU+9XftQ==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-common@5.55.2': + resolution: {integrity: sha512-9L4IpIYUqA63a7sw1trnHQGUvwiAjKz67nsgDnal98JGAc7wyposRb0Iag+eiMuyzFFaSHLe2/rGyIo+PafRBA==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-insights@5.55.2': + resolution: {integrity: sha512-ZBm2ytY5EHFcj+kjNsXxMNO/TGlOHe2fBFXGKHJOM1bk1rAy4o2YI+d9oV/w/jrqx44pvJMJlc8X6vKnCuDgUQ==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-personalization@5.55.2': + resolution: {integrity: sha512-3FGVW/jDk7sdYwqa2NKnF/qXWcttc4bvGrwNbvqz3VoWSRv42CNvRk+3Y9QJFIUf1vY50hAuVWUoFKdyc8vaXA==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-query-suggestions@5.55.2': + resolution: {integrity: sha512-JsG8LovDAYul5t8e533tZ3O1uZILxso5zsTtB7ONc5RJ8ACdTxAAC/jaOnsBNYb+x+STP7fzx/Iro55v5DNgoQ==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-search@5.55.2': + resolution: {integrity: sha512-5wDnoIfC75zJ2MSHv5SSzTlRL2z7jQMbqQ5jrzottuq2p3oBObv8pD/JpXWu8pRaimaxNr3/Bs/KZIGVXxJ7hg==} + engines: {node: '>= 14.0.0'} + + '@algolia/ingestion@1.55.2': + resolution: {integrity: sha512-da+SC6ikpza98W7C5ChsKEQDvZc8PQLQ0sxmQ5yMRsHpdD3iPKnclJA6ViB5Nr5T9qOX+IDswC6AyqY4V3rtug==} + engines: {node: '>= 14.0.0'} + + '@algolia/monitoring@1.55.2': + resolution: {integrity: sha512-Y8kEcPqCiIEeaGv83l9RRA09mfYECqAJHNnOyEtZc9UirI6XBMUyFVss/sSeYUiV/Lf30hkbWcl00V1uXsf86Q==} + engines: {node: '>= 14.0.0'} + + '@algolia/recommend@5.55.2': + resolution: {integrity: sha512-5zmobuCQqFZkx+84Nt+suL7vo6jTh2CfAs2ndDSeTS2QHvnzP8YEEGWtWftjyACI0cK/FuH8urWwCHP+d2j8TA==} + engines: {node: '>= 14.0.0'} + + '@algolia/requester-browser-xhr@5.55.2': + resolution: {integrity: sha512-qnGUUuWG66dRMnr33owLsrYIh9fHVxtU4R2rd3SpneAHuoAUcGbDOWNrj05glVU6M8yOqo9gQ22K8zpz0I8Xpg==} + engines: {node: '>= 14.0.0'} + + '@algolia/requester-fetch@5.55.2': + resolution: {integrity: sha512-lKZ5uhafMvR7dWCJEyuaeyZitid1I3ICx+k0vGf5x/ktdIQvc7bndCiOPpmIDqUmN26FE3jTehkAzSqee95G2Q==} + engines: {node: '>= 14.0.0'} + + '@algolia/requester-node-http@5.55.2': + resolution: {integrity: sha512-Zc90xvKWUvxcNicvvTO9Pr/hT2TAnkixOIzJm/KMj5Ptm2pKjk71ngTsdkbRtJQvhZ2Kr9N1YdIjLrNHB5P2xw==} + engines: {node: '>= 14.0.0'} + '@antfu/install-pkg@1.1.0': resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} @@ -2818,6 +2906,29 @@ packages: resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} engines: {node: '>=20.19.0'} + '@docsearch/css@3.8.2': + resolution: {integrity: sha512-y05ayQFyUmCXze79+56v/4HpycYF3uFqB78pLPrSV5ZKAlDuIAAJNhaRi8tTdRNXh05yxX/TyNnzD6LwSM89vQ==} + + '@docsearch/js@3.8.2': + resolution: {integrity: sha512-Q5wY66qHn0SwA7Taa0aDbHiJvaFJLOJyHmooQ7y8hlwwQLQ/5WwCcoX0g7ii04Qi2DJlHsd0XXzJ8Ypw9+9YmQ==} + + '@docsearch/react@3.8.2': + resolution: {integrity: sha512-xCRrJQlTt8N9GU0DG4ptwHRkfnSnD/YpdeaXe02iKfqs97TkZJv60yE+1eq/tjPcVnTW8dP5qLP7itifFVV5eg==} + peerDependencies: + '@types/react': '>= 16.8.0 < 19.0.0' + react: '>= 16.8.0 < 19.0.0' + react-dom: '>= 16.8.0 < 19.0.0' + search-insights: '>= 1 < 3' + peerDependenciesMeta: + '@types/react': + optional: true + react: + optional: true + react-dom: + optional: true + search-insights: + optional: true + '@earendil-works/pi-ai@0.79.3': resolution: {integrity: sha512-lMSput/haP5uZAGbXhS5rAYd3GB7GYdJkoAUxg3VFummBeqGqGqllaTWrbHFN12kVGyVfWHhdySNXkiqVh65Iw==} engines: {node: '>=22.19.0'} @@ -2841,102 +2952,204 @@ packages: '@emnapi/wasi-threads@1.2.2': resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + '@esbuild/aix-ppc64@0.28.1': resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm64@0.28.1': resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} engines: {node: '>=18'} cpu: [arm64] os: [android] + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + '@esbuild/android-arm@0.28.1': resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} engines: {node: '>=18'} cpu: [arm] os: [android] + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + '@esbuild/android-x64@0.28.1': resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} engines: {node: '>=18'} cpu: [x64] os: [android] + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-arm64@0.28.1': resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + '@esbuild/darwin-x64@0.28.1': resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-arm64@0.28.1': resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + '@esbuild/freebsd-x64@0.28.1': resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm64@0.28.1': resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} engines: {node: '>=18'} cpu: [arm64] os: [linux] + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + '@esbuild/linux-arm@0.28.1': resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} engines: {node: '>=18'} cpu: [arm] os: [linux] + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-ia32@0.28.1': resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} engines: {node: '>=18'} cpu: [ia32] os: [linux] + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-loong64@0.28.1': resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} engines: {node: '>=18'} cpu: [loong64] os: [linux] + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-mips64el@0.28.1': resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-ppc64@0.28.1': resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-riscv64@0.28.1': resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-s390x@0.28.1': resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} engines: {node: '>=18'} cpu: [s390x] os: [linux] + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + '@esbuild/linux-x64@0.28.1': resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} engines: {node: '>=18'} @@ -2949,6 +3162,12 @@ packages: cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + '@esbuild/netbsd-x64@0.28.1': resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} engines: {node: '>=18'} @@ -2961,6 +3180,12 @@ packages: cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + '@esbuild/openbsd-x64@0.28.1': resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} engines: {node: '>=18'} @@ -2973,24 +3198,48 @@ packages: cpu: [arm64] os: [openharmony] + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + '@esbuild/sunos-x64@0.28.1': resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} engines: {node: '>=18'} cpu: [x64] os: [sunos] + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-arm64@0.28.1': resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} engines: {node: '>=18'} cpu: [arm64] os: [win32] + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-ia32@0.28.1': resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} engines: {node: '>=18'} cpu: [ia32] os: [win32] + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + '@esbuild/win32-x64@0.28.1': resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} engines: {node: '>=18'} @@ -3071,6 +3320,9 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} + '@iconify-json/simple-icons@1.2.90': + resolution: {integrity: sha512-zt2o2ZvQpHVvZJARIkZ51RnaHY2oqcPJMvHE+mVnxkSr+c33fnX4gciiXu+wyX5ei+s0qbVX1wD0DWBbaGBYMA==} + '@iconify/types@2.0.0': resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} @@ -3594,6 +3846,168 @@ packages: '@rolldown/pluginutils@1.0.1': resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + '@rollup/rollup-android-arm-eabi@4.62.2': + resolution: {integrity: sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.62.2': + resolution: {integrity: sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.62.2': + resolution: {integrity: sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.62.2': + resolution: {integrity: sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.62.2': + resolution: {integrity: sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.62.2': + resolution: {integrity: sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.62.2': + resolution: {integrity: sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.62.2': + resolution: {integrity: sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.62.2': + resolution: {integrity: sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.62.2': + resolution: {integrity: sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.62.2': + resolution: {integrity: sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.62.2': + resolution: {integrity: sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.62.2': + resolution: {integrity: sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.62.2': + resolution: {integrity: sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.62.2': + resolution: {integrity: sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.62.2': + resolution: {integrity: sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.62.2': + resolution: {integrity: sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.62.2': + resolution: {integrity: sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.62.2': + resolution: {integrity: sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.62.2': + resolution: {integrity: sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.62.2': + resolution: {integrity: sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.62.2': + resolution: {integrity: sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.62.2': + resolution: {integrity: sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.62.2': + resolution: {integrity: sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.62.2': + resolution: {integrity: sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==} + cpu: [x64] + os: [win32] + + '@shikijs/core@2.5.0': + resolution: {integrity: sha512-uu/8RExTKtavlpH7XqnVYBrfBkUc20ngXiX9NSrBhOVZYv/7XQRKUyhtkeflY5QsxC0GbJThCerruZfsUaSldg==} + + '@shikijs/engine-javascript@2.5.0': + resolution: {integrity: sha512-VjnOpnQf8WuCEZtNUdjjwGUbtAVKuZkVQ/5cHy/tojVVRIRtlWMYVjyWhxOmIq05AlSOv72z7hRNRGVBgQOl0w==} + + '@shikijs/engine-oniguruma@2.5.0': + resolution: {integrity: sha512-pGd1wRATzbo/uatrCIILlAdFVKdxImWJGQ5rFiB5VZi2ve5xj3Ax9jny8QvkaV93btQEwR/rSz5ERFpC5mKNIw==} + + '@shikijs/langs@2.5.0': + resolution: {integrity: sha512-Qfrrt5OsNH5R+5tJ/3uYBBZv3SuGmnRPejV9IlIbFH3HTGLDlkqgHymAlzklVmKBjAaVmkPkyikAV/sQ1wSL+w==} + + '@shikijs/themes@2.5.0': + resolution: {integrity: sha512-wGrk+R8tJnO0VMzmUExHR+QdSaPUl/NKs+a4cQQRWyoc3YFbUzuLEi/KWK1hj+8BfHRKm2jNhhJck1dfstJpiw==} + + '@shikijs/transformers@2.5.0': + resolution: {integrity: sha512-SI494W5X60CaUwgi8u4q4m4s3YAFSxln3tzNjOSYqq54wlVgz0/NbbXEb3mdLbqMBztcmS7bVTaEd2w0qMmfeg==} + + '@shikijs/types@2.5.0': + resolution: {integrity: sha512-ygl5yhxki9ZLNuNpPitBWvcy9fsSKKaRuO4BAlMyagszQidxcpLAr0qiW/q43DtSIDxO6hEbtYLiFZNXO/hdGw==} + + '@shikijs/vscode-textmate@10.0.2': + resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + '@smithy/core@3.24.7': resolution: {integrity: sha512-KoUi4M1f3BG6kzN1FnCwL7oyFptTbyBJKjR6yhSib+JHRdUmM1o+VwsFtJ66NZCkCzVfJMWRHJNo0R0jznp0Pg==} engines: {node: '>=18.0.0'} @@ -3760,6 +4174,9 @@ packages: '@types/geojson@7946.0.16': resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} + '@types/hast@3.0.5': + resolution: {integrity: sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==} + '@types/js-yaml@4.0.9': resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} @@ -3772,9 +4189,18 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/linkify-it@5.0.0': + resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} + + '@types/markdown-it@14.1.2': + resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} + '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + '@types/mdurl@2.0.0': + resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} + '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} @@ -3799,6 +4225,9 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/web-bluetooth@0.0.21': + resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} + '@typescript-eslint/eslint-plugin@8.61.0': resolution: {integrity: sha512-bFNvl9ZczlVb+wR2Akszf3gHfKVj/8WanXaGJ3UstTA7brNKg0cNdk6X1Psu5V7MZ2oQtzZKOEzIUehaoxbDGw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -3858,9 +4287,19 @@ packages: resolution: {integrity: sha512-QVLZu3ZPQEE+HICQyAMZ2yLQhxf0meY/wx6Hx14YcTNj13JB3qHlX3lJ02L3fLGHgERRH71kvYDwiXIguT3AjQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@ungap/structured-clone@1.3.3': + resolution: {integrity: sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==} + '@upsetjs/venn.js@2.0.0': resolution: {integrity: sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==} + '@vitejs/plugin-vue@5.2.4': + resolution: {integrity: sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==} + engines: {node: ^18.0.0 || >=20.0.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 + vue: ^3.2.25 + '@vitest/coverage-v8@4.1.8': resolution: {integrity: sha512-lt3kovsyHwYe00wq4D1ti0Z974fWj4NLp6siqiyEufUpyFwK9Yhi7rBhac9JL5aA0zoMrJqc4vYPZRUnI7l7nw==} peerDependencies: @@ -3899,6 +4338,98 @@ packages: '@vitest/utils@4.1.8': resolution: {integrity: sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==} + '@vue/compiler-core@3.5.39': + resolution: {integrity: sha512-16KBTEXAJCpDr0mwlw+AZyhu8iyC7R3S2vBwsI7QnWJU6X3WKc9VKeNEZpiMdZ569qWhz9574L3vV55qRL0Vtw==} + + '@vue/compiler-dom@3.5.39': + resolution: {integrity: sha512-oQPigALqYbNxTNPvNgSOe+czwVExfbVF02lz8jP0S3AXJiu3jxYDygNUiqSep4ezzW8XgnubqH63My2A7JR/vg==} + + '@vue/compiler-sfc@3.5.39': + resolution: {integrity: sha512-d0ki86iOyN8LoZPBmk5SJWNwHP19CnDDCfuo//+2WJa2g5Ke0Jay983PIBIcSSzldC68I8DrD5GrHV3OSDfodg==} + + '@vue/compiler-ssr@3.5.39': + resolution: {integrity: sha512-Ce7/wvwMHai74bdszfXExdazFigYnlF9zgCmEQUcM1j0fOymlouZ7XilTYNo8oUjhlnjYOZbGrcYKuqjz89Ucw==} + + '@vue/devtools-api@7.7.10': + resolution: {integrity: sha512-KxtEpUOOpFz/qOGRrAwA36QF7DqIA+FXgCYit9mk9wjbaZt0sXOFz81ElOZtKA4HbWHUdwNjZHBFsFFyp5BZiA==} + + '@vue/devtools-kit@7.7.10': + resolution: {integrity: sha512-3WNi2Kq4tbpVbmhml7RiphmAt0279oh3fKNeWMQIrltfX8Q91b4i5PL8DtyNKdwmcsGrV4fg+erwWOmD05CLIw==} + + '@vue/devtools-shared@7.7.10': + resolution: {integrity: sha512-wOPslzB8vTvpxwdaOcR2qAbwmuSP0L+rhpoC6Cf56V3Jip+HWb7PQQXOUPgBNQARpXsbQX/+mvi8kKucmBGRwQ==} + + '@vue/reactivity@3.5.39': + resolution: {integrity: sha512-TpsuBJ9gGlZa5d23XcM2y8EXanz9dZeVDQBXRwzy46ItgvM+rWpzs+UVM0wcRLxGvcav0HE5jz2gNL53xlRAog==} + + '@vue/runtime-core@3.5.39': + resolution: {integrity: sha512-9GLtNyRvPAUMbX+7ono0RC2j0guo2LXVi8LvcmAooImACUKm0oFf0jjwbX8/H0AE/t1nxhAkn8RSl9PMCzzxZw==} + + '@vue/runtime-dom@3.5.39': + resolution: {integrity: sha512-7Y6aAGboKcXAZ3ECuUy7RrS5yy2r47dhTp2SKaJmYxjopImaVFaNa5Ne66NwGovsrxVAl5S5rwc7m22UG7Lmww==} + + '@vue/server-renderer@3.5.39': + resolution: {integrity: sha512-yZSakiAGw85rZfG7UM8akMnIF+FmeiNk47uvHf2nVBBSe+dIKUhZuZq9+XgJhbV3nS5Z4ALH23/MpXofW+mbcw==} + peerDependencies: + vue: 3.5.39 + + '@vue/shared@3.5.39': + resolution: {integrity: sha512-l1rrBtBfTnmxvtsvdQDXltUUy8S1Y+ZaqdfUzmAnJkTd8Z8rv5v/ytW+TKiqEOWyHPoqtPlNFSs0lhRmYVSHVA==} + + '@vueuse/core@12.8.2': + resolution: {integrity: sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==} + + '@vueuse/integrations@12.8.2': + resolution: {integrity: sha512-fbGYivgK5uBTRt7p5F3zy6VrETlV9RtZjBqd1/HxGdjdckBgBM4ugP8LHpjolqTj14TXTxSK1ZfgPbHYyGuH7g==} + peerDependencies: + async-validator: ^4 + axios: ^1 + change-case: ^5 + drauu: ^0.4 + focus-trap: ^7 + fuse.js: ^7 + idb-keyval: ^6 + jwt-decode: ^4 + nprogress: ^0.2 + qrcode: ^1.5 + sortablejs: ^1 + universal-cookie: ^7 + peerDependenciesMeta: + async-validator: + optional: true + axios: + optional: true + change-case: + optional: true + drauu: + optional: true + focus-trap: + optional: true + fuse.js: + optional: true + idb-keyval: + optional: true + jwt-decode: + optional: true + nprogress: + optional: true + qrcode: + optional: true + sortablejs: + optional: true + universal-cookie: + optional: true + + '@vueuse/metadata@12.8.2': + resolution: {integrity: sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==} + + '@vueuse/shared@12.8.2': + resolution: {integrity: sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==} + + '@xmldom/xmldom@0.9.10': + resolution: {integrity: sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==} + engines: {node: '>=14.6'} + accepts@2.0.0: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} @@ -3931,6 +4462,14 @@ packages: ajv@8.20.0: resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + algoliasearch@5.55.2: + resolution: {integrity: sha512-OyacJsaeuLUvGWOynNqYc6sx88XvyoG39wMT8SYqL3l9wwaorDW/LPRbUPfhzw0bWsUWzNCZTnFYOrWFBKsUaw==} + engines: {node: '>= 14.0.0'} + + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -3984,6 +4523,9 @@ packages: bignumber.js@9.3.1: resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} + birpc@2.9.0: + resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==} + birpc@4.0.0: resolution: {integrity: sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw==} @@ -3991,6 +4533,9 @@ packages: resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==} engines: {node: '>=18'} + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + bowser@2.14.1: resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} @@ -4031,9 +4576,22 @@ packages: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + character-entities@2.0.2: resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + cheerio-select@1.6.0: + resolution: {integrity: sha512-eq0GdBvxVFbqWgmCm7M3XGs1I8oLy/nExUnh6oLqmBditPO9AqQJrkslDpMun/hZ0yyTs8L0m85OHp4ho6Qm9g==} + + cheerio@1.0.0-rc.10: + resolution: {integrity: sha512-g0J0q/O6mW8z5zxQ3A8E8J1hUgp4SMOvEoW/x84OwyHKe/Zccz83PVT4y5Crcr530FV6NgmKI1qvGTKVl9XXVw==} + engines: {node: '>= 6'} + chokidar@4.0.3: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} @@ -4045,10 +4603,21 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + + commander@13.1.0: + resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} + engines: {node: '>=18'} + commander@15.0.0: resolution: {integrity: sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==} engines: {node: '>=22.12.0'} + commander@6.2.1: + resolution: {integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==} + engines: {node: '>= 6'} + commander@7.2.0: resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} engines: {node: '>= 10'} @@ -4080,6 +4649,10 @@ packages: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} + copy-anything@4.0.5: + resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==} + engines: {node: '>=18'} + cordis@4.0.0-rc.7: resolution: {integrity: sha512-5nm6ehrSfJhEUV659CctEvyNuBY/AXapw8+ZEw7YENztdzpiT+Ha8nIfkyhfyAgPtJns9aB5On5nzl9Sm6zHeQ==} hasBin: true @@ -4112,10 +4685,20 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + css-select@4.3.0: + resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==} + css-tree@3.2.1: resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + css-what@6.2.2: + resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} + engines: {node: '>= 6'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + cytoscape-cose-bilkent@4.1.0: resolution: {integrity: sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==} peerDependencies: @@ -4330,9 +4913,26 @@ packages: resolution: {integrity: sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==} engines: {node: '>=0.3.1'} + dom-serializer@1.4.1: + resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@3.3.0: + resolution: {integrity: sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA==} + engines: {node: '>= 4'} + + domhandler@4.3.1: + resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==} + engines: {node: '>= 4'} + dompurify@3.4.11: resolution: {integrity: sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==} + domutils@2.8.0: + resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} + dts-resolver@3.0.0: resolution: {integrity: sha512-1T1f+z+4tl9XD+m+0HBgWoL/nm0bOIffyWaUuUSBlFg/86IWvfx+wjNaO/ybU0AJzG9/Mi5hBUgGV6zCmWEN7Q==} engines: {node: ^22.18.0 || >=24.0.0} @@ -4355,6 +4955,9 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + emoji-regex-xs@1.0.0: + resolution: {integrity: sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -4369,6 +4972,13 @@ packages: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} + entities@2.2.0: + resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} + + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + entities@8.0.0: resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==} engines: {node: '>=20.19.0'} @@ -4391,11 +5001,20 @@ packages: es-toolkit@1.49.0: resolution: {integrity: sha512-G5iZ6Pc/FNRY/soKZHC+TxGDD83rHUDXxzaWhGCX44vAv/tMs56WMusnm/KMNK+luUPsgA9U28cGr4RDlSzL2g==} + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + esbuild@0.28.1: resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} engines: {node: '>=18'} hasBin: true + escape-goat@3.0.0: + resolution: {integrity: sha512-w3PwNZJwRxlp47QGzhuEBldEqVHHhh8/tIPcl6ecf2Bou99cdAt0knihBV0Ecc7CGxYduXVBDheH1K2oADRlvw==} + engines: {node: '>=10'} + escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} @@ -4438,6 +5057,10 @@ packages: jiti: optional: true + esm@3.2.25: + resolution: {integrity: sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==} + engines: {node: '>=6'} + espree@10.4.0: resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -4458,6 +5081,9 @@ packages: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} @@ -4561,6 +5187,9 @@ packages: flatted@3.4.2: resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + focus-trap@7.8.0: + resolution: {integrity: sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA==} + foreground-child@3.3.1: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} @@ -4664,10 +5293,19 @@ packages: resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} engines: {node: '>= 0.4'} + hast-util-to-html@9.0.5: + resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + hono@4.12.29: resolution: {integrity: sha512-1hNiRjawYrLq/4m3DQQjPGFg0VZkk4RjQJDff/excI6Dm9BiL75qxGrd7/c6YOxPdq6AscP3LiXhQ6fKFC1Waw==} engines: {node: '>=16.9.0'} + hookable@5.5.3: + resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + hookable@6.1.1: resolution: {integrity: sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ==} @@ -4678,6 +5316,15 @@ packages: html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + + htmlparser2@5.0.1: + resolution: {integrity: sha512-vKZZra6CSe9qsJzh0BjBGXo8dvzNsq/oGvsjfRdOrrryfeD9UOBEEQdeoqCRmKZchF5h2zOBMQ6YuQ0uRUmdbQ==} + + htmlparser2@6.1.0: + resolution: {integrity: sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==} + http-errors@2.0.1: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} @@ -4756,6 +5403,10 @@ packages: is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-what@5.5.0: + resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==} + engines: {node: '>=18'} + isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} @@ -4878,6 +5529,11 @@ packages: jszip@3.10.1: resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} + juice@8.1.0: + resolution: {integrity: sha512-FLzurJrx5Iv1e7CfBSZH68dC04EEvXvvVvPYB7Vx1WAuhCp1ZPIMtqxc+WTWxVkpTIC2Ach/GAv0rQbtGf6YMA==} + engines: {node: '>=10.0.0'} + hasBin: true + jwa@2.0.1: resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} @@ -5073,6 +5729,12 @@ packages: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} + mark.js@8.11.1: + resolution: {integrity: sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==} + + markdown-it-mathjax3@4.3.2: + resolution: {integrity: sha512-TX3GW5NjmupgFtMJGRauioMbbkGsOXAAt1DZ/rzzYmTHqzkO1rNAdiMD4NiruurToPApn2kYy76x02QN26qr2w==} + markdown-table@3.0.4: resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} @@ -5085,6 +5747,10 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + mathjax-full@3.2.2: + resolution: {integrity: sha512-+LfG9Fik+OuI8SLwsiR02IVdjcnRCy5MufYLi0C3TdMT56L/pjB0alMVGgoWJF8pN9Rc7FESycZB9BMNWIid5w==} + deprecated: Version 4 replaces this package with the scoped package @mathjax/src + mdast-util-find-and-replace@3.0.2: resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} @@ -5112,6 +5778,9 @@ packages: mdast-util-phrasing@4.1.0: resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + mdast-util-to-markdown@2.1.2: resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} @@ -5125,6 +5794,9 @@ packages: resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} engines: {node: '>= 0.8'} + mensch@0.3.4: + resolution: {integrity: sha512-IAeFvcOnV9V0Yk+bFhYR07O3yNina9ANIN5MoXBKYJ/RLYPurd2d0yw14MDhpr9/momp0WofT1bPUh3hkzdi/g==} + merge-descriptors@2.0.0: resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} engines: {node: '>=18'} @@ -5132,6 +5804,9 @@ packages: mermaid@11.16.0: resolution: {integrity: sha512-Zvm3kbstgdpvIJPPItlL7fppIZ3kibvc1oZIGxdvk9t6UFz6flv+Jw7FtRGKwfcI8OckmH04LqG6LlS6X4B1pA==} + mhchemparser@4.2.1: + resolution: {integrity: sha512-kYmyrCirqJf3zZ9t/0wGgRZ4/ZJw//VwaRVGA75C4nhE60vtnIzhl9J9ndkX/h6hxSN7pjg/cE0VxbnNM+bnDQ==} + micromark-core-commonmark@2.0.3: resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} @@ -5224,6 +5899,11 @@ packages: resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} engines: {node: '>=18'} + mime@2.6.0: + resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} + engines: {node: '>=4.0.0'} + hasBin: true + minimatch@10.2.5: resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} engines: {node: 18 || 20 || >=22} @@ -5239,6 +5919,15 @@ packages: resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} engines: {node: '>=16 || 14 >=14.17'} + minisearch@7.2.0: + resolution: {integrity: sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg==} + + mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + + mj-context-menu@0.6.1: + resolution: {integrity: sha512-7NO5s6n10TIV96d4g2uDpG7ZDpIhMh0QNfGdJw/W47JswFcosz457wqz/b5sAKvl12sxINGFCn80NZHKwxQEXA==} + mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} @@ -5334,10 +6023,22 @@ packages: engines: {node: '>=10.5.0'} deprecated: Use your platform's native DOMException instead + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + node-fetch@3.3.2: resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -5357,6 +6058,9 @@ packages: once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + oniguruma-to-es@3.1.1: + resolution: {integrity: sha512-bUH8SDvPkH3ho3dvwJwfonjlQ4R80vjyvrU8YpxuROddv55vAEJrTuCuCVUhhsHbtlD9tGGbaNApGQckXhS8iQ==} + openai@6.26.0: resolution: {integrity: sha512-zd23dbWTjiJ6sSAX6s0HrCZi41JwTA1bQVs0wLQPZ2/5o2gxOJA5wh7yOAUgwYybfhDXyhwlpeQf7Mlgx8EOCA==} hasBin: true @@ -5401,6 +6105,12 @@ packages: pako@1.0.11: resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + parse5-htmlparser2-tree-adapter@6.0.1: + resolution: {integrity: sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==} + + parse5@6.0.1: + resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} + parse5@8.0.1: resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==} @@ -5436,6 +6146,9 @@ packages: pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + perfect-debounce@1.0.0: + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -5457,6 +6170,14 @@ packages: resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} engines: {node: ^10 || ^12 || >=14} + preact@10.29.7: + resolution: {integrity: sha512-DCHYrK/B10yUD3ZjLfhZ3WIE/9Vf9VFUODcRE2dRomTYDpJk6z6L9wecSfhfE6M9ZTHUdyQkoC46arIDhEV84Q==} + peerDependencies: + preact-render-to-string: '>=5' + peerDependenciesMeta: + preact-render-to-string: + optional: true + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -5464,6 +6185,9 @@ packages: process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + property-information@7.2.0: + resolution: {integrity: sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==} + protobufjs@7.6.4: resolution: {integrity: sha512-RJJPTTpvFfHcWLkIa2JFWK4XvtSzS0yEWDmunqHXli1h3JlkbcQZXDZdcWxv+JK3Xsl5/UFDPZ0iGm7DAengYw==} engines: {node: '>=12.0.0'} @@ -5510,6 +6234,15 @@ packages: resolution: {integrity: sha512-J8rn6v4DBb2nnFqkqwy6/NnTYMcgLA+sLr0iIO41qpv0n+ngb7ksag2tMRl0inb1bbO/esUwzW1vbJi7K0sI0g==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + regex-recursion@6.0.2: + resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} + + regex-utilities@2.3.0: + resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} + + regex@6.1.0: + resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} + regexp-ast-analysis@0.7.1: resolution: {integrity: sha512-sZuz1dYW/ZsfG17WSAG7eS85r5a0dDsvg+7BiiYR5o6lKCAtUrEwdmRmaGF6rwVj3LcmAeYkOWKEPlbPzN3Y3A==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} @@ -5525,6 +6258,9 @@ packages: resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} engines: {node: '>= 4'} + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + robust-predicates@3.0.3: resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==} @@ -5557,6 +6293,11 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true + rollup@4.62.2: + resolution: {integrity: sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + roughjs@4.6.6: resolution: {integrity: sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==} @@ -5591,6 +6332,9 @@ packages: resolution: {integrity: sha512-3A6sD0WYP7+QrjbfNA2FN3FsOaGGFoekCVgTyypy53gPxhbkCIjtO6YWgdrfM+n/8sI8JeXZOIxsHjMTNxQ4nQ==} engines: {node: ^14.0.0 || >=16.0.0} + search-insights@2.17.3: + resolution: {integrity: sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==} + semver@7.8.4: resolution: {integrity: sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==} engines: {node: '>=10'} @@ -5618,6 +6362,9 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + shiki@2.5.0: + resolution: {integrity: sha512-mI//trrsaiCIPsja5CNfsyNOqgAZUb6VpJA+340toL42UpzQlXpwRV9nch69X6gaUxrr9kaOOa6e3y3uAkGFxQ==} + side-channel-list@1.0.1: resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} engines: {node: '>= 0.4'} @@ -5644,6 +6391,9 @@ packages: sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + slick@1.12.2: + resolution: {integrity: sha512-4qdtOGcBjral6YIBCWJ0ljFSKNLz9KkhbWtuGvUyRowl1kxfuE1x/Z/aJcaiilpb3do9bl5K7/1h9XC5wWpY/A==} + smol-toml@1.6.1: resolution: {integrity: sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==} engines: {node: '>= 18'} @@ -5656,6 +6406,17 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + + speakingurl@14.0.1: + resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} + engines: {node: '>=0.10.0'} + + speech-rule-engine@4.1.4: + resolution: {integrity: sha512-i/VCLG1fvRc95pMHRqG4aQNscv+9aIsqA2oI7ZQS51sTdUcDHYX6cpT8/tqZ+enjs1tKVwbRBWgxut9SWn+f9g==} + hasBin: true + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} @@ -5677,6 +6438,9 @@ packages: string_decoder@1.1.1: resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -5695,6 +6459,10 @@ packages: stylis@4.4.0: resolution: {integrity: sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==} + superjson@2.2.6: + resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==} + engines: {node: '>=16'} + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -5706,6 +6474,9 @@ packages: symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + tabbable@6.5.0: + resolution: {integrity: sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA==} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -5736,6 +6507,9 @@ packages: resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==} engines: {node: '>=16'} + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + tr46@6.0.0: resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} engines: {node: '>=20'} @@ -5744,6 +6518,9 @@ packages: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + ts-algebra@2.0.0: resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==} @@ -5858,6 +6635,9 @@ packages: unist-util-is@6.0.1: resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + unist-util-stringify-position@4.0.0: resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} @@ -5881,15 +6661,56 @@ packages: resolution: {integrity: sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==} hasBin: true + valid-data-url@3.0.1: + resolution: {integrity: sha512-jOWVmzVceKlVVdwjNSenT4PbGghU0SBIizAev8ofZVgivk/TVHXSbNL8LP6M3spZvkR9/QolkyJavGSX5Cs0UA==} + engines: {node: '>=10'} + vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + vite-tsconfig-paths@6.1.1: resolution: {integrity: sha512-2cihq7zliibCCZ8P9cKJrQBkfgdvcFkOOc3Y02o3GWUDLgqjWsZudaoiuOwO/gzTzy17cS5F7ZPo4bsnS4DGkg==} peerDependencies: vite: '*' + vite@5.4.21: + resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + vite@8.0.16: resolution: {integrity: sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -5933,6 +6754,18 @@ packages: yaml: optional: true + vitepress@1.6.4: + resolution: {integrity: sha512-+2ym1/+0VVrbhNyRoFFesVvBvHAVMZMK0rw60E3X/5349M1GuVdKeazuksqopEdvkKwKGs21Q729jX81/bkBJg==} + hasBin: true + peerDependencies: + markdown-it-mathjax3: ^4 + postcss: ^8 + peerDependenciesMeta: + markdown-it-mathjax3: + optional: true + postcss: + optional: true + vitest@4.1.8: resolution: {integrity: sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -5974,6 +6807,14 @@ packages: jsdom: optional: true + vue@3.5.39: + resolution: {integrity: sha512-xmZCYabFGcirU8r0fTuvl/LICc1OU620rnqepaJDL/a141ZigkG7AyaxQLdqJ02ZRYzWe6YPaDHeQx7MfknQfA==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + w3c-xmlserializer@5.0.0: resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} engines: {node: '>=18'} @@ -5982,10 +6823,17 @@ packages: resolution: {integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==} engines: {node: 20 || >=22} + web-resource-inliner@6.0.1: + resolution: {integrity: sha512-kfqDxt5dTB1JhqsCUQVFDj0rmY+4HLwGQIsLPbyrsN9y9WV/1oFDSx3BQ4GfCv9X+jVeQ7rouTqwK53rA/7t8A==} + engines: {node: '>=10.0.0'} + web-streams-polyfill@3.3.3: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + webidl-conversions@8.0.1: resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} engines: {node: '>=20'} @@ -5998,6 +6846,9 @@ packages: resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -6008,6 +6859,9 @@ packages: engines: {node: '>=8'} hasBin: true + wicked-good-xpath@1.3.0: + resolution: {integrity: sha512-Gd9+TUn5nXdwj/hFsPVx5cuHHiF5Bwuc30jZ4+ronF1qHK5O7HD0sgmXWSEgwKquT3ClLoKPVbO6qGwVwLzvAw==} + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -6075,6 +6929,118 @@ snapshots: dependencies: zod: 4.4.3 + '@algolia/abtesting@1.21.2': + dependencies: + '@algolia/client-common': 5.55.2 + '@algolia/requester-browser-xhr': 5.55.2 + '@algolia/requester-fetch': 5.55.2 + '@algolia/requester-node-http': 5.55.2 + + '@algolia/autocomplete-core@1.17.7(@algolia/client-search@5.55.2)(algoliasearch@5.55.2)(search-insights@2.17.3)': + dependencies: + '@algolia/autocomplete-plugin-algolia-insights': 1.17.7(@algolia/client-search@5.55.2)(algoliasearch@5.55.2)(search-insights@2.17.3) + '@algolia/autocomplete-shared': 1.17.7(@algolia/client-search@5.55.2)(algoliasearch@5.55.2) + transitivePeerDependencies: + - '@algolia/client-search' + - algoliasearch + - search-insights + + '@algolia/autocomplete-plugin-algolia-insights@1.17.7(@algolia/client-search@5.55.2)(algoliasearch@5.55.2)(search-insights@2.17.3)': + dependencies: + '@algolia/autocomplete-shared': 1.17.7(@algolia/client-search@5.55.2)(algoliasearch@5.55.2) + search-insights: 2.17.3 + transitivePeerDependencies: + - '@algolia/client-search' + - algoliasearch + + '@algolia/autocomplete-preset-algolia@1.17.7(@algolia/client-search@5.55.2)(algoliasearch@5.55.2)': + dependencies: + '@algolia/autocomplete-shared': 1.17.7(@algolia/client-search@5.55.2)(algoliasearch@5.55.2) + '@algolia/client-search': 5.55.2 + algoliasearch: 5.55.2 + + '@algolia/autocomplete-shared@1.17.7(@algolia/client-search@5.55.2)(algoliasearch@5.55.2)': + dependencies: + '@algolia/client-search': 5.55.2 + algoliasearch: 5.55.2 + + '@algolia/client-abtesting@5.55.2': + dependencies: + '@algolia/client-common': 5.55.2 + '@algolia/requester-browser-xhr': 5.55.2 + '@algolia/requester-fetch': 5.55.2 + '@algolia/requester-node-http': 5.55.2 + + '@algolia/client-analytics@5.55.2': + dependencies: + '@algolia/client-common': 5.55.2 + '@algolia/requester-browser-xhr': 5.55.2 + '@algolia/requester-fetch': 5.55.2 + '@algolia/requester-node-http': 5.55.2 + + '@algolia/client-common@5.55.2': {} + + '@algolia/client-insights@5.55.2': + dependencies: + '@algolia/client-common': 5.55.2 + '@algolia/requester-browser-xhr': 5.55.2 + '@algolia/requester-fetch': 5.55.2 + '@algolia/requester-node-http': 5.55.2 + + '@algolia/client-personalization@5.55.2': + dependencies: + '@algolia/client-common': 5.55.2 + '@algolia/requester-browser-xhr': 5.55.2 + '@algolia/requester-fetch': 5.55.2 + '@algolia/requester-node-http': 5.55.2 + + '@algolia/client-query-suggestions@5.55.2': + dependencies: + '@algolia/client-common': 5.55.2 + '@algolia/requester-browser-xhr': 5.55.2 + '@algolia/requester-fetch': 5.55.2 + '@algolia/requester-node-http': 5.55.2 + + '@algolia/client-search@5.55.2': + dependencies: + '@algolia/client-common': 5.55.2 + '@algolia/requester-browser-xhr': 5.55.2 + '@algolia/requester-fetch': 5.55.2 + '@algolia/requester-node-http': 5.55.2 + + '@algolia/ingestion@1.55.2': + dependencies: + '@algolia/client-common': 5.55.2 + '@algolia/requester-browser-xhr': 5.55.2 + '@algolia/requester-fetch': 5.55.2 + '@algolia/requester-node-http': 5.55.2 + + '@algolia/monitoring@1.55.2': + dependencies: + '@algolia/client-common': 5.55.2 + '@algolia/requester-browser-xhr': 5.55.2 + '@algolia/requester-fetch': 5.55.2 + '@algolia/requester-node-http': 5.55.2 + + '@algolia/recommend@5.55.2': + dependencies: + '@algolia/client-common': 5.55.2 + '@algolia/requester-browser-xhr': 5.55.2 + '@algolia/requester-fetch': 5.55.2 + '@algolia/requester-node-http': 5.55.2 + + '@algolia/requester-browser-xhr@5.55.2': + dependencies: + '@algolia/client-common': 5.55.2 + + '@algolia/requester-fetch@5.55.2': + dependencies: + '@algolia/client-common': 5.55.2 + + '@algolia/requester-node-http@5.55.2': + dependencies: + '@algolia/client-common': 5.55.2 + '@antfu/install-pkg@1.1.0': dependencies: package-manager-detector: 1.6.0 @@ -6445,6 +7411,31 @@ snapshots: '@csstools/css-tokenizer@4.0.0': {} + '@docsearch/css@3.8.2': {} + + '@docsearch/js@3.8.2(@algolia/client-search@5.55.2)(search-insights@2.17.3)': + dependencies: + '@docsearch/react': 3.8.2(@algolia/client-search@5.55.2)(search-insights@2.17.3) + preact: 10.29.7 + transitivePeerDependencies: + - '@algolia/client-search' + - '@types/react' + - preact-render-to-string + - react + - react-dom + - search-insights + + '@docsearch/react@3.8.2(@algolia/client-search@5.55.2)(search-insights@2.17.3)': + dependencies: + '@algolia/autocomplete-core': 1.17.7(@algolia/client-search@5.55.2)(algoliasearch@5.55.2)(search-insights@2.17.3) + '@algolia/autocomplete-preset-algolia': 1.17.7(@algolia/client-search@5.55.2)(algoliasearch@5.55.2) + '@docsearch/css': 3.8.2 + algoliasearch: 5.55.2 + optionalDependencies: + search-insights: 2.17.3 + transitivePeerDependencies: + - '@algolia/client-search' + '@earendil-works/pi-ai@0.79.3(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3)': dependencies: '@anthropic-ai/sdk': 0.91.1(zod@4.4.3) @@ -6497,81 +7488,150 @@ snapshots: tslib: 2.8.1 optional: true + '@esbuild/aix-ppc64@0.21.5': + optional: true + '@esbuild/aix-ppc64@0.28.1': optional: true + '@esbuild/android-arm64@0.21.5': + optional: true + '@esbuild/android-arm64@0.28.1': optional: true + '@esbuild/android-arm@0.21.5': + optional: true + '@esbuild/android-arm@0.28.1': optional: true + '@esbuild/android-x64@0.21.5': + optional: true + '@esbuild/android-x64@0.28.1': optional: true + '@esbuild/darwin-arm64@0.21.5': + optional: true + '@esbuild/darwin-arm64@0.28.1': optional: true + '@esbuild/darwin-x64@0.21.5': + optional: true + '@esbuild/darwin-x64@0.28.1': optional: true + '@esbuild/freebsd-arm64@0.21.5': + optional: true + '@esbuild/freebsd-arm64@0.28.1': optional: true + '@esbuild/freebsd-x64@0.21.5': + optional: true + '@esbuild/freebsd-x64@0.28.1': optional: true + '@esbuild/linux-arm64@0.21.5': + optional: true + '@esbuild/linux-arm64@0.28.1': optional: true + '@esbuild/linux-arm@0.21.5': + optional: true + '@esbuild/linux-arm@0.28.1': optional: true + '@esbuild/linux-ia32@0.21.5': + optional: true + '@esbuild/linux-ia32@0.28.1': optional: true + '@esbuild/linux-loong64@0.21.5': + optional: true + '@esbuild/linux-loong64@0.28.1': optional: true + '@esbuild/linux-mips64el@0.21.5': + optional: true + '@esbuild/linux-mips64el@0.28.1': optional: true + '@esbuild/linux-ppc64@0.21.5': + optional: true + '@esbuild/linux-ppc64@0.28.1': optional: true + '@esbuild/linux-riscv64@0.21.5': + optional: true + '@esbuild/linux-riscv64@0.28.1': optional: true + '@esbuild/linux-s390x@0.21.5': + optional: true + '@esbuild/linux-s390x@0.28.1': optional: true + '@esbuild/linux-x64@0.21.5': + optional: true + '@esbuild/linux-x64@0.28.1': optional: true '@esbuild/netbsd-arm64@0.28.1': optional: true + '@esbuild/netbsd-x64@0.21.5': + optional: true + '@esbuild/netbsd-x64@0.28.1': optional: true '@esbuild/openbsd-arm64@0.28.1': optional: true + '@esbuild/openbsd-x64@0.21.5': + optional: true + '@esbuild/openbsd-x64@0.28.1': optional: true '@esbuild/openharmony-arm64@0.28.1': optional: true + '@esbuild/sunos-x64@0.21.5': + optional: true + '@esbuild/sunos-x64@0.28.1': optional: true + '@esbuild/win32-arm64@0.21.5': + optional: true + '@esbuild/win32-arm64@0.28.1': optional: true + '@esbuild/win32-ia32@0.21.5': + optional: true + '@esbuild/win32-ia32@0.28.1': optional: true + '@esbuild/win32-x64@0.21.5': + optional: true + '@esbuild/win32-x64@0.28.1': optional: true @@ -6640,6 +7700,10 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} + '@iconify-json/simple-icons@1.2.90': + dependencies: + '@iconify/types': 2.0.0 + '@iconify/types@2.0.0': {} '@iconify/utils@3.1.3': @@ -7002,6 +8066,121 @@ snapshots: '@rolldown/pluginutils@1.0.1': {} + '@rollup/rollup-android-arm-eabi@4.62.2': + optional: true + + '@rollup/rollup-android-arm64@4.62.2': + optional: true + + '@rollup/rollup-darwin-arm64@4.62.2': + optional: true + + '@rollup/rollup-darwin-x64@4.62.2': + optional: true + + '@rollup/rollup-freebsd-arm64@4.62.2': + optional: true + + '@rollup/rollup-freebsd-x64@4.62.2': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.62.2': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.62.2': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-x64-musl@4.62.2': + optional: true + + '@rollup/rollup-openbsd-x64@4.62.2': + optional: true + + '@rollup/rollup-openharmony-arm64@4.62.2': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.62.2': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.62.2': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.62.2': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.62.2': + optional: true + + '@shikijs/core@2.5.0': + dependencies: + '@shikijs/engine-javascript': 2.5.0 + '@shikijs/engine-oniguruma': 2.5.0 + '@shikijs/types': 2.5.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.5 + hast-util-to-html: 9.0.5 + + '@shikijs/engine-javascript@2.5.0': + dependencies: + '@shikijs/types': 2.5.0 + '@shikijs/vscode-textmate': 10.0.2 + oniguruma-to-es: 3.1.1 + + '@shikijs/engine-oniguruma@2.5.0': + dependencies: + '@shikijs/types': 2.5.0 + '@shikijs/vscode-textmate': 10.0.2 + + '@shikijs/langs@2.5.0': + dependencies: + '@shikijs/types': 2.5.0 + + '@shikijs/themes@2.5.0': + dependencies: + '@shikijs/types': 2.5.0 + + '@shikijs/transformers@2.5.0': + dependencies: + '@shikijs/core': 2.5.0 + '@shikijs/types': 2.5.0 + + '@shikijs/types@2.5.0': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.5 + + '@shikijs/vscode-textmate@10.0.2': {} + '@smithy/core@3.24.7': dependencies: '@aws-crypto/crc32': 5.2.0 @@ -7209,6 +8388,10 @@ snapshots: '@types/geojson@7946.0.16': {} + '@types/hast@3.0.5': + dependencies: + '@types/unist': 3.0.3 + '@types/js-yaml@4.0.9': {} '@types/jsdom@28.0.3': @@ -7222,10 +8405,19 @@ snapshots: '@types/json-schema@7.0.15': {} + '@types/linkify-it@5.0.0': {} + + '@types/markdown-it@14.1.2': + dependencies: + '@types/linkify-it': 5.0.0 + '@types/mdurl': 2.0.0 + '@types/mdast@4.0.4': dependencies: '@types/unist': 3.0.3 + '@types/mdurl@2.0.0': {} + '@types/ms@2.1.0': {} '@types/node@22.20.0': @@ -7247,6 +8439,8 @@ snapshots: '@types/unist@3.0.3': {} + '@types/web-bluetooth@0.0.21': {} + '@typescript-eslint/eslint-plugin@8.61.0(@typescript-eslint/parser@8.61.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -7338,11 +8532,18 @@ snapshots: '@typescript-eslint/types': 8.61.0 eslint-visitor-keys: 5.0.1 + '@ungap/structured-clone@1.3.3': {} + '@upsetjs/venn.js@2.0.0': optionalDependencies: d3-selection: 3.0.0 d3-transition: 3.0.1(d3-selection@3.0.0) + '@vitejs/plugin-vue@5.2.4(vite@5.4.21(@types/node@25.9.3)(lightningcss@1.32.0))(vue@3.5.39(typescript@6.0.3))': + dependencies: + vite: 5.4.21(@types/node@25.9.3)(lightningcss@1.32.0) + vue: 3.5.39(typescript@6.0.3) + '@vitest/coverage-v8@4.1.8(vitest@4.1.8)': dependencies: '@bcoe/v8-coverage': 1.0.2 @@ -7406,6 +8607,107 @@ snapshots: convert-source-map: 2.0.0 tinyrainbow: 3.1.0 + '@vue/compiler-core@3.5.39': + dependencies: + '@babel/parser': 7.29.7 + '@vue/shared': 3.5.39 + entities: 7.0.1 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.39': + dependencies: + '@vue/compiler-core': 3.5.39 + '@vue/shared': 3.5.39 + + '@vue/compiler-sfc@3.5.39': + dependencies: + '@babel/parser': 7.29.7 + '@vue/compiler-core': 3.5.39 + '@vue/compiler-dom': 3.5.39 + '@vue/compiler-ssr': 3.5.39 + '@vue/shared': 3.5.39 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.15 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.39': + dependencies: + '@vue/compiler-dom': 3.5.39 + '@vue/shared': 3.5.39 + + '@vue/devtools-api@7.7.10': + dependencies: + '@vue/devtools-kit': 7.7.10 + + '@vue/devtools-kit@7.7.10': + dependencies: + '@vue/devtools-shared': 7.7.10 + birpc: 2.9.0 + hookable: 5.5.3 + mitt: 3.0.1 + perfect-debounce: 1.0.0 + speakingurl: 14.0.1 + superjson: 2.2.6 + + '@vue/devtools-shared@7.7.10': + dependencies: + rfdc: 1.4.1 + + '@vue/reactivity@3.5.39': + dependencies: + '@vue/shared': 3.5.39 + + '@vue/runtime-core@3.5.39': + dependencies: + '@vue/reactivity': 3.5.39 + '@vue/shared': 3.5.39 + + '@vue/runtime-dom@3.5.39': + dependencies: + '@vue/reactivity': 3.5.39 + '@vue/runtime-core': 3.5.39 + '@vue/shared': 3.5.39 + csstype: 3.2.3 + + '@vue/server-renderer@3.5.39(vue@3.5.39(typescript@6.0.3))': + dependencies: + '@vue/compiler-ssr': 3.5.39 + '@vue/shared': 3.5.39 + vue: 3.5.39(typescript@6.0.3) + + '@vue/shared@3.5.39': {} + + '@vueuse/core@12.8.2(typescript@6.0.3)': + dependencies: + '@types/web-bluetooth': 0.0.21 + '@vueuse/metadata': 12.8.2 + '@vueuse/shared': 12.8.2(typescript@6.0.3) + vue: 3.5.39(typescript@6.0.3) + transitivePeerDependencies: + - typescript + + '@vueuse/integrations@12.8.2(focus-trap@7.8.0)(typescript@6.0.3)': + dependencies: + '@vueuse/core': 12.8.2(typescript@6.0.3) + '@vueuse/shared': 12.8.2(typescript@6.0.3) + vue: 3.5.39(typescript@6.0.3) + optionalDependencies: + focus-trap: 7.8.0 + transitivePeerDependencies: + - typescript + + '@vueuse/metadata@12.8.2': {} + + '@vueuse/shared@12.8.2(typescript@6.0.3)': + dependencies: + vue: 3.5.39(typescript@6.0.3) + transitivePeerDependencies: + - typescript + + '@xmldom/xmldom@0.9.10': {} + accepts@2.0.0: dependencies: mime-types: 3.0.2 @@ -7437,6 +8739,25 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 + algoliasearch@5.55.2: + dependencies: + '@algolia/abtesting': 1.21.2 + '@algolia/client-abtesting': 5.55.2 + '@algolia/client-analytics': 5.55.2 + '@algolia/client-common': 5.55.2 + '@algolia/client-insights': 5.55.2 + '@algolia/client-personalization': 5.55.2 + '@algolia/client-query-suggestions': 5.55.2 + '@algolia/client-search': 5.55.2 + '@algolia/ingestion': 1.55.2 + '@algolia/monitoring': 1.55.2 + '@algolia/recommend': 5.55.2 + '@algolia/requester-browser-xhr': 5.55.2 + '@algolia/requester-fetch': 5.55.2 + '@algolia/requester-node-http': 5.55.2 + + ansi-colors@4.1.3: {} + ansi-regex@5.0.1: {} ansi-regex@6.2.2: {} @@ -7479,6 +8800,8 @@ snapshots: bignumber.js@9.3.1: {} + birpc@2.9.0: {} + birpc@4.0.0: {} body-parser@2.3.0: @@ -7495,6 +8818,8 @@ snapshots: transitivePeerDependencies: - supports-color + boolbase@1.0.0: {} + bowser@2.14.1: {} brace-expansion@2.1.2: @@ -7527,8 +8852,30 @@ snapshots: chai@6.2.2: {} + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + character-entities@2.0.2: {} + cheerio-select@1.6.0: + dependencies: + css-select: 4.3.0 + css-what: 6.2.2 + domelementtype: 2.3.0 + domhandler: 4.3.1 + domutils: 2.8.0 + + cheerio@1.0.0-rc.10: + dependencies: + cheerio-select: 1.6.0 + dom-serializer: 1.4.1 + domhandler: 4.3.1 + htmlparser2: 6.1.0 + parse5: 6.0.1 + parse5-htmlparser2-tree-adapter: 6.0.1 + tslib: 2.8.1 + chokidar@4.0.3: dependencies: readdirp: 4.1.2 @@ -7539,8 +8886,14 @@ snapshots: color-name@1.1.4: {} + comma-separated-tokens@2.0.3: {} + + commander@13.1.0: {} + commander@15.0.0: {} + commander@6.2.1: {} + commander@7.2.0: {} commander@8.3.0: {} @@ -7557,6 +8910,10 @@ snapshots: cookie@0.7.2: {} + copy-anything@4.0.5: + dependencies: + is-what: 5.5.0 + cordis@4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5): dependencies: '@standard-schema/spec': 1.1.0 @@ -7604,11 +8961,23 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + css-select@4.3.0: + dependencies: + boolbase: 1.0.0 + css-what: 6.2.2 + domhandler: 4.3.1 + domutils: 2.8.0 + nth-check: 2.1.1 + css-tree@3.2.1: dependencies: mdn-data: 2.27.1 source-map-js: 1.2.1 + css-what@6.2.2: {} + + csstype@3.2.3: {} + cytoscape-cose-bilkent@4.1.0(cytoscape@3.34.0): dependencies: cose-base: 1.0.3 @@ -7836,10 +9205,32 @@ snapshots: diff@9.0.0: {} + dom-serializer@1.4.1: + dependencies: + domelementtype: 2.3.0 + domhandler: 4.3.1 + entities: 2.2.0 + + domelementtype@2.3.0: {} + + domhandler@3.3.0: + dependencies: + domelementtype: 2.3.0 + + domhandler@4.3.1: + dependencies: + domelementtype: 2.3.0 + dompurify@3.4.11: optionalDependencies: '@types/trusted-types': 2.0.7 + domutils@2.8.0: + dependencies: + dom-serializer: 1.4.1 + domelementtype: 2.3.0 + domhandler: 4.3.1 + dts-resolver@3.0.0(oxc-resolver@11.20.0): optionalDependencies: oxc-resolver: 11.20.0 @@ -7858,6 +9249,8 @@ snapshots: ee-first@1.1.1: {} + emoji-regex-xs@1.0.0: {} + emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} @@ -7866,6 +9259,10 @@ snapshots: encodeurl@2.0.0: {} + entities@2.2.0: {} + + entities@7.0.1: {} + entities@8.0.0: {} es-define-property@1.0.1: {} @@ -7880,6 +9277,32 @@ snapshots: es-toolkit@1.49.0: {} + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + esbuild@0.28.1: optionalDependencies: '@esbuild/aix-ppc64': 0.28.1 @@ -7909,6 +9332,8 @@ snapshots: '@esbuild/win32-ia32': 0.28.1 '@esbuild/win32-x64': 0.28.1 + escape-goat@3.0.0: {} + escape-html@1.0.3: {} escape-string-regexp@4.0.0: {} @@ -7982,6 +9407,8 @@ snapshots: transitivePeerDependencies: - supports-color + esm@3.2.25: {} + espree@10.4.0: dependencies: acorn: 8.17.0 @@ -8004,6 +9431,8 @@ snapshots: estraverse@5.3.0: {} + estree-walker@2.0.2: {} + estree-walker@3.0.3: dependencies: '@types/estree': 1.0.9 @@ -8134,6 +9563,10 @@ snapshots: flatted@3.4.2: {} + focus-trap@7.8.0: + dependencies: + tabbable: 6.5.0 + foreground-child@3.3.1: dependencies: cross-spawn: 7.0.6 @@ -8251,8 +9684,28 @@ snapshots: dependencies: function-bind: 1.1.2 + hast-util-to-html@9.0.5: + dependencies: + '@types/hast': 3.0.5 + '@types/unist': 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.1 + property-information: 7.2.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.5 + hono@4.12.29: {} + hookable@5.5.3: {} + hookable@6.1.1: {} html-encoding-sniffer@6.0.0: @@ -8263,6 +9716,22 @@ snapshots: html-escaper@2.0.2: {} + html-void-elements@3.0.0: {} + + htmlparser2@5.0.1: + dependencies: + domelementtype: 2.3.0 + domhandler: 3.3.0 + domutils: 2.8.0 + entities: 2.2.0 + + htmlparser2@6.1.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 4.3.1 + domutils: 2.8.0 + entities: 2.2.0 + http-errors@2.0.1: dependencies: depd: 2.0.0 @@ -8327,6 +9796,8 @@ snapshots: is-promise@4.0.0: {} + is-what@5.5.0: {} + isarray@1.0.0: {} isexe@2.0.0: {} @@ -8447,6 +9918,16 @@ snapshots: readable-stream: 2.3.8 setimmediate: 1.0.5 + juice@8.1.0: + dependencies: + cheerio: 1.0.0-rc.10 + commander: 6.2.1 + mensch: 0.3.4 + slick: 1.12.2 + web-resource-inliner: 6.0.1 + transitivePeerDependencies: + - encoding + jwa@2.0.1: dependencies: buffer-equal-constant-time: 1.0.1 @@ -8619,12 +10100,28 @@ snapshots: dependencies: semver: 7.8.4 + mark.js@8.11.1: {} + + markdown-it-mathjax3@4.3.2: + dependencies: + juice: 8.1.0 + mathjax-full: 3.2.2 + transitivePeerDependencies: + - encoding + markdown-table@3.0.4: {} marked@16.4.2: {} math-intrinsics@1.1.0: {} + mathjax-full@3.2.2: + dependencies: + esm: 3.2.25 + mhchemparser: 4.2.1 + mj-context-menu: 0.6.1 + speech-rule-engine: 4.1.4 + mdast-util-find-and-replace@3.0.2: dependencies: '@types/mdast': 4.0.4 @@ -8711,6 +10208,18 @@ snapshots: '@types/mdast': 4.0.4 unist-util-is: 6.0.1 + mdast-util-to-hast@13.2.1: + dependencies: + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.3 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + mdast-util-to-markdown@2.1.2: dependencies: '@types/mdast': 4.0.4 @@ -8731,6 +10240,8 @@ snapshots: media-typer@1.1.0: {} + mensch@0.3.4: {} + merge-descriptors@2.0.0: {} mermaid@11.16.0: @@ -8757,6 +10268,8 @@ snapshots: ts-dedent: 2.3.0 uuid: 14.0.1 + mhchemparser@4.2.1: {} + micromark-core-commonmark@2.0.3: dependencies: decode-named-character-reference: 1.3.0 @@ -8954,6 +10467,8 @@ snapshots: dependencies: mime-db: 1.54.0 + mime@2.6.0: {} + minimatch@10.2.5: dependencies: brace-expansion: 5.0.6 @@ -8966,6 +10481,12 @@ snapshots: minipass@7.1.3: {} + minisearch@7.2.0: {} + + mitt@3.0.1: {} + + mj-context-menu@0.6.1: {} + mri@1.2.0: {} ms@2.1.3: {} @@ -9040,12 +10561,20 @@ snapshots: node-domexception@1.0.0: {} + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + node-fetch@3.3.2: dependencies: data-uri-to-buffer: 4.0.1 fetch-blob: 3.2.0 formdata-polyfill: 4.0.10 + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + object-assign@4.1.1: {} object-inspect@1.13.4: {} @@ -9060,6 +10589,12 @@ snapshots: dependencies: wrappy: 1.0.2 + oniguruma-to-es@3.1.1: + dependencies: + emoji-regex-xs: 1.0.0 + regex: 6.1.0 + regex-recursion: 6.0.2 + openai@6.26.0(ws@8.21.0)(zod@4.4.3): optionalDependencies: ws: 8.21.0 @@ -9140,6 +10675,12 @@ snapshots: pako@1.0.11: {} + parse5-htmlparser2-tree-adapter@6.0.1: + dependencies: + parse5: 6.0.1 + + parse5@6.0.1: {} + parse5@8.0.1: dependencies: entities: 8.0.0 @@ -9165,6 +10706,8 @@ snapshots: pathe@2.0.3: {} + perfect-debounce@1.0.0: {} + picocolors@1.1.1: {} picomatch@4.0.4: {} @@ -9184,10 +10727,14 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + preact@10.29.7: {} + prelude-ls@1.2.1: {} process-nextick-args@2.0.1: {} + property-information@7.2.0: {} + protobufjs@7.6.4: dependencies: '@protobufjs/aspromise': 1.1.2 @@ -9250,6 +10797,16 @@ snapshots: dependencies: '@eslint-community/regexpp': 4.12.2 + regex-recursion@6.0.2: + dependencies: + regex-utilities: 2.3.0 + + regex-utilities@2.3.0: {} + + regex@6.1.0: + dependencies: + regex-utilities: 2.3.0 + regexp-ast-analysis@0.7.1: dependencies: '@eslint-community/regexpp': 4.12.2 @@ -9261,6 +10818,8 @@ snapshots: retry@0.13.1: {} + rfdc@1.4.1: {} + robust-predicates@3.0.3: {} rolldown-plugin-dts@0.25.2(oxc-resolver@11.20.0)(rolldown@1.1.1)(typescript@6.0.3): @@ -9321,6 +10880,37 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.1.1 '@rolldown/binding-win32-x64-msvc': 1.1.1 + rollup@4.62.2: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.62.2 + '@rollup/rollup-android-arm64': 4.62.2 + '@rollup/rollup-darwin-arm64': 4.62.2 + '@rollup/rollup-darwin-x64': 4.62.2 + '@rollup/rollup-freebsd-arm64': 4.62.2 + '@rollup/rollup-freebsd-x64': 4.62.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.62.2 + '@rollup/rollup-linux-arm-musleabihf': 4.62.2 + '@rollup/rollup-linux-arm64-gnu': 4.62.2 + '@rollup/rollup-linux-arm64-musl': 4.62.2 + '@rollup/rollup-linux-loong64-gnu': 4.62.2 + '@rollup/rollup-linux-loong64-musl': 4.62.2 + '@rollup/rollup-linux-ppc64-gnu': 4.62.2 + '@rollup/rollup-linux-ppc64-musl': 4.62.2 + '@rollup/rollup-linux-riscv64-gnu': 4.62.2 + '@rollup/rollup-linux-riscv64-musl': 4.62.2 + '@rollup/rollup-linux-s390x-gnu': 4.62.2 + '@rollup/rollup-linux-x64-gnu': 4.62.2 + '@rollup/rollup-linux-x64-musl': 4.62.2 + '@rollup/rollup-openbsd-x64': 4.62.2 + '@rollup/rollup-openharmony-arm64': 4.62.2 + '@rollup/rollup-win32-arm64-msvc': 4.62.2 + '@rollup/rollup-win32-ia32-msvc': 4.62.2 + '@rollup/rollup-win32-x64-gnu': 4.62.2 + '@rollup/rollup-win32-x64-msvc': 4.62.2 + fsevents: 2.3.3 + roughjs@4.6.6: dependencies: hachure-fill: 0.5.2 @@ -9365,6 +10955,8 @@ snapshots: refa: 0.12.1 regexp-ast-analysis: 0.7.1 + search-insights@2.17.3: {} + semver@7.8.4: {} send@1.2.1: @@ -9402,6 +10994,17 @@ snapshots: shebang-regex@3.0.0: {} + shiki@2.5.0: + dependencies: + '@shikijs/core': 2.5.0 + '@shikijs/engine-javascript': 2.5.0 + '@shikijs/engine-oniguruma': 2.5.0 + '@shikijs/langs': 2.5.0 + '@shikijs/themes': 2.5.0 + '@shikijs/types': 2.5.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.5 + side-channel-list@1.0.1: dependencies: es-errors: 1.3.0 @@ -9436,12 +11039,24 @@ snapshots: sisteransi@1.0.5: {} + slick@1.12.2: {} + smol-toml@1.6.1: {} source-map-js@1.2.1: {} source-map@0.6.1: {} + space-separated-tokens@2.0.2: {} + + speakingurl@14.0.1: {} + + speech-rule-engine@4.1.4: + dependencies: + '@xmldom/xmldom': 0.9.10 + commander: 13.1.0 + wicked-good-xpath: 1.3.0 + stackback@0.0.2: {} statuses@2.0.2: {} @@ -9464,6 +11079,11 @@ snapshots: dependencies: safe-buffer: 5.1.2 + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -9480,6 +11100,10 @@ snapshots: stylis@4.4.0: {} + superjson@2.2.6: + dependencies: + copy-anything: 4.0.5 + supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -9488,6 +11112,8 @@ snapshots: symbol-tree@3.2.4: {} + tabbable@6.5.0: {} + tinybench@2.9.0: {} tinyexec@1.2.4: {} @@ -9511,12 +11137,16 @@ snapshots: dependencies: tldts: 7.4.5 + tr46@0.0.3: {} + tr46@6.0.0: dependencies: punycode: 2.3.1 tree-kill@1.2.2: {} + trim-lines@3.0.1: {} + ts-algebra@2.0.0: {} ts-api-utils@2.5.0(typescript@6.0.3): @@ -9609,6 +11239,10 @@ snapshots: dependencies: '@types/unist': 3.0.3 + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position@4.0.0: dependencies: '@types/unist': 3.0.3 @@ -9634,8 +11268,20 @@ snapshots: uuid@14.0.1: {} + valid-data-url@3.0.1: {} + vary@1.1.2: {} + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + vite-tsconfig-paths@6.1.1(typescript@6.0.3)(vite@8.0.16(@types/node@22.20.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)): dependencies: debug: 4.4.3 @@ -9646,6 +11292,16 @@ snapshots: - supports-color - typescript + vite@5.4.21(@types/node@25.9.3)(lightningcss@1.32.0): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.15 + rollup: 4.62.2 + optionalDependencies: + '@types/node': 25.9.3 + fsevents: 2.3.3 + lightningcss: 1.32.0 + vite@8.0.16(@types/node@22.20.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0): dependencies: lightningcss: 1.32.0 @@ -9676,6 +11332,57 @@ snapshots: tsx: 4.22.4 yaml: 2.9.0 + vitepress@1.6.4(@algolia/client-search@5.55.2)(@types/node@25.9.3)(lightningcss@1.32.0)(markdown-it-mathjax3@4.3.2)(postcss@8.5.15)(search-insights@2.17.3)(typescript@6.0.3): + dependencies: + '@docsearch/css': 3.8.2 + '@docsearch/js': 3.8.2(@algolia/client-search@5.55.2)(search-insights@2.17.3) + '@iconify-json/simple-icons': 1.2.90 + '@shikijs/core': 2.5.0 + '@shikijs/transformers': 2.5.0 + '@shikijs/types': 2.5.0 + '@types/markdown-it': 14.1.2 + '@vitejs/plugin-vue': 5.2.4(vite@5.4.21(@types/node@25.9.3)(lightningcss@1.32.0))(vue@3.5.39(typescript@6.0.3)) + '@vue/devtools-api': 7.7.10 + '@vue/shared': 3.5.39 + '@vueuse/core': 12.8.2(typescript@6.0.3) + '@vueuse/integrations': 12.8.2(focus-trap@7.8.0)(typescript@6.0.3) + focus-trap: 7.8.0 + mark.js: 8.11.1 + minisearch: 7.2.0 + shiki: 2.5.0 + vite: 5.4.21(@types/node@25.9.3)(lightningcss@1.32.0) + vue: 3.5.39(typescript@6.0.3) + optionalDependencies: + markdown-it-mathjax3: 4.3.2 + postcss: 8.5.15 + transitivePeerDependencies: + - '@algolia/client-search' + - '@types/node' + - '@types/react' + - async-validator + - axios + - change-case + - drauu + - fuse.js + - idb-keyval + - jwt-decode + - less + - lightningcss + - nprogress + - preact-render-to-string + - qrcode + - react + - react-dom + - sass + - sass-embedded + - search-insights + - sortablejs + - stylus + - sugarss + - terser + - typescript + - universal-cookie + vitest@4.1.8(@types/node@22.20.0)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@8.0.16(@types/node@22.20.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.8 @@ -9734,14 +11441,37 @@ snapshots: transitivePeerDependencies: - msw + vue@3.5.39(typescript@6.0.3): + dependencies: + '@vue/compiler-dom': 3.5.39 + '@vue/compiler-sfc': 3.5.39 + '@vue/runtime-dom': 3.5.39 + '@vue/server-renderer': 3.5.39(vue@3.5.39(typescript@6.0.3)) + '@vue/shared': 3.5.39 + optionalDependencies: + typescript: 6.0.3 + w3c-xmlserializer@5.0.0: dependencies: xml-name-validator: 5.0.0 walk-up-path@4.0.0: {} + web-resource-inliner@6.0.1: + dependencies: + ansi-colors: 4.1.3 + escape-goat: 3.0.0 + htmlparser2: 5.0.1 + mime: 2.6.0 + node-fetch: 2.7.0 + valid-data-url: 3.0.1 + transitivePeerDependencies: + - encoding + web-streams-polyfill@3.3.3: {} + webidl-conversions@3.0.1: {} + webidl-conversions@8.0.1: {} whatwg-mimetype@5.0.0: {} @@ -9754,6 +11484,11 @@ snapshots: transitivePeerDependencies: - '@noble/hashes' + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + which@2.0.2: dependencies: isexe: 2.0.0 @@ -9763,6 +11498,8 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 + wicked-good-xpath@1.3.0: {} + word-wrap@1.2.5: {} wordwrap@1.0.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index bda50a6c69..07182c9959 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,6 +1,7 @@ packages: - vendor/* - packages/*/* + - website # Deploy root of the single-exe build: a pure dependency manifest whose # closure is what the exe bundles and what the Python runtime distributes. - python/sdk-runtime diff --git a/scripts/cordis-walk.ts b/scripts/cordis-walk.ts new file mode 100644 index 0000000000..44e87b2a21 --- /dev/null +++ b/scripts/cordis-walk.ts @@ -0,0 +1,94 @@ +/** + * Shared AST walkers for the cordis documentation generators + * (`gen-cordis-catalog.ts`, `gen-website-api.ts`): locating the cordis module + * merge in a source file, enumerating its `interface Events` members, and + * resolving the `interface Context` service keys to their service classes. + * One walk, two renderers — the catalog and the website page carry different + * prose but must agree on WHAT exists. + */ + +import ts from 'typescript' +import { parseJsDoc, pointer, rawJsDoc } from './jsdoc.ts' + +/** The body of the cordis module merge in `sf`: `declare module 'cordis'` + * (harness packages) or `declare module './context.ts'` (vendor core), or + * null when the file has neither. */ +export function cordisModuleBody(sf: ts.SourceFile): ts.ModuleBlock | null { + for (const stmt of sf.statements) { + if (!ts.isModuleDeclaration(stmt) || !ts.isStringLiteral(stmt.name)) continue + if (stmt.name.text !== 'cordis' && stmt.name.text !== './context.ts') continue + if (stmt.body && ts.isModuleBlock(stmt.body)) return stmt.body + } + return null +} + +/** Every `interface Events` method member of a cordis module merge, with the + * event name resolved from its (possibly string-literal) property name. */ +export function eventMembers(body: ts.ModuleBlock, sf: ts.SourceFile): { name: string; member: ts.MethodSignature }[] { + const out: { name: string; member: ts.MethodSignature }[] = [] + for (const stmt of body.statements) { + if (!ts.isInterfaceDeclaration(stmt) || stmt.name.text !== 'Events') continue + for (const member of stmt.members) { + if (!ts.isMethodSignature(member)) continue + const name = ts.isStringLiteral(member.name) ? member.name.text : member.name.getText(sf) + out.push({ name, member }) + } + } + return out +} + +/** The `ctx. → type name` map declared by a merge's `interface Context`. */ +function contextKeyMap(body: ts.ModuleBlock, sf: ts.SourceFile): Map { + const keyToType = new Map() + for (const stmt of body.statements) { + if (!ts.isInterfaceDeclaration(stmt) || stmt.name.text !== 'Context') continue + for (const member of stmt.members) { + if (!ts.isPropertySignature(member) || !member.type) continue + keyToType.set(member.name.getText(sf), member.type.getText(sf)) + } + } + return keyToType +} + +/** One `ctx.` service class resolved from a Context merge. */ +export interface ServiceClass { + key: string + type: string + cls: ts.ClassDeclaration + abstract: boolean + /** Class-level JSDoc prose (empty string when missing — also reported). */ + doc: string +} + +/** + * Resolve each `ctx.` of a merge to the service class declared in the + * same file. A key whose type is not a class here (a Pick-mixin member, e.g. + * timer helpers) is skipped. A class without JSDoc prose is reported into + * `violations` (named `where` by the caller's gate). + * + * @param body — the cordis module merge body. + * @param sf — the source file containing the merge. + * @param rel — repo-relative path of `sf`, for violation pointers. + * @param violations — sink for JSDoc-completeness violations. + * @returns the resolved service classes, in Context-declaration order. + */ +export function serviceClasses( + body: ts.ModuleBlock, + sf: ts.SourceFile, + rel: string, + violations: string[], +): ServiceClass[] { + const text = sf.getFullText() + const out: ServiceClass[] = [] + for (const [key, type] of contextKeyMap(body, sf)) { + const cls = sf.statements.find( + (s): s is ts.ClassDeclaration => ts.isClassDeclaration(s) && s.name?.text === type, + ) + if (!cls) continue // a Pick-mixin member, not a class here + const abstract = cls.modifiers?.some(m => m.kind === ts.SyntaxKind.AbstractKeyword) ?? false + const doc = parseJsDoc(rawJsDoc(text, cls)).doc + if (!doc) violations.push(`service ctx.${key} (${pointer(rel, sf, cls)}): class ${type} has no JSDoc.`) + out.push({ key, type, cls, abstract, doc }) + } + return out +} diff --git a/scripts/doc-typecheck.ts b/scripts/doc-typecheck.ts index 94c9dc0723..7549ee982b 100644 --- a/scripts/doc-typecheck.ts +++ b/scripts/doc-typecheck.ts @@ -8,6 +8,7 @@ import { execFileSync } from 'node:child_process' import { globSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' import { join, relative, resolve } from 'node:path' import ts from 'typescript' +import { extractFences } from './md-fences.ts' const root = resolve(import.meta.dirname, '..') @@ -27,39 +28,21 @@ interface Block { code: string } +/** The info-string → kind table this gate tracks. */ +const KIND_BY_INFO: Record = { + 'ts': 'check', + 'ts ignore-check': 'ignore', + 'ts type-equiv': 'type-equiv', + 'ts cordis-catalog': 'cordis-catalog', + 'ts persistence-catalog': 'persistence-catalog', + 'ts config-catalog': 'config-catalog', +} + /** Extract every recognized TypeScript fence from one Markdown file. */ function extractBlocks(absPath: string): Block[] { - const text = readFileSync(absPath, 'utf8') - const lines = text.split('\n') const file = relative(root, absPath) - const blocks: Block[] = [] - let open: { line: number; kind: BlockKind; body: string[] } | null = null - - lines.forEach((raw, i) => { - const fence = /^```(\s*)(\S.*)?$/.exec(raw) - if (!fence) { - if (open) open.body.push(raw) - return - } - if (open) { - // closing fence - blocks.push({ file, line: open.line, kind: open.kind, code: open.body.join('\n') }) - open = null - return - } - // Ignore non-TypeScript fences. - const info = (fence[2] ?? '').trim() - const kind: BlockKind | null = - info === 'ts' ? 'check' - : info === 'ts ignore-check' ? 'ignore' - : info === 'ts type-equiv' ? 'type-equiv' - : info === 'ts cordis-catalog' ? 'cordis-catalog' - : info === 'ts persistence-catalog' ? 'persistence-catalog' - : info === 'ts config-catalog' ? 'config-catalog' - : null - if (kind) open = { line: i + 1, kind, body: [] } - }) - return blocks + return extractFences(absPath, info => KIND_BY_INFO[info] ?? null) + .map(f => ({ file, line: f.line, kind: f.kind, code: f.code })) } const configHost: ts.ParseConfigFileHost = { @@ -208,7 +191,7 @@ function remapBlockPaths(output: string, blocks: Block[]): string { }) } -const markdownGlobs = ['README.md', 'docs/**/*.md', 'packages/*/*.md', 'packages/*/*/*.md'] +const markdownGlobs = ['README.md', 'docs/**/*.md', 'packages/*/*.md', 'packages/*/*/*.md', 'website/zh-CN/**/*.md'] const files: string[] = [] for (const pattern of markdownGlobs) { diff --git a/scripts/gen-cordis-catalog.ts b/scripts/gen-cordis-catalog.ts index be265b1014..2c31d95f52 100644 --- a/scripts/gen-cordis-catalog.ts +++ b/scripts/gen-cordis-catalog.ts @@ -9,6 +9,7 @@ import { globSync, readFileSync, writeFileSync } from 'node:fs' import { resolve, sep } from 'node:path' import ts from 'typescript' import { checkParams, checkReturns, parseJsDoc, parseTags, pointer, rawJsDoc, reportViolations, type Mode } from './jsdoc.ts' +import { cordisModuleBody, eventMembers, serviceClasses } from './cordis-walk.ts' const root = resolve(import.meta.dirname, '..') const OUT_EVENTS = 'docs/cordis-catalog/events.md' @@ -102,15 +103,8 @@ interface InheritedEntry { source: string } -/** Find the `declare module 'cordis'` body in a source file, or null. */ -function cordisModuleBody(sf: ts.SourceFile): ts.ModuleBlock | null { - for (const stmt of sf.statements) { - if (ts.isModuleDeclaration(stmt) && ts.isStringLiteral(stmt.name) && stmt.name.text === 'cordis') { - if (stmt.body && ts.isModuleBlock(stmt.body)) return stmt.body - } - } - return null -} +// cordisModuleBody / eventMembers / serviceClasses live in cordis-walk.ts, +// shared with gen-website-api.ts — one walk, two renderers. /** The signature text of a method-signature member (everything but a body). */ function memberSignature(member: ts.TypeElement | ts.ClassElement, sf: ts.SourceFile): string { @@ -134,38 +128,33 @@ export function collectEvents(scanRoot: string = root): EventEntry[] { const sf = ts.createSourceFile(abs, text, ts.ScriptTarget.Latest, true) const body = cordisModuleBody(sf) if (!body) continue - for (const stmt of body.statements) { - if (!ts.isInterfaceDeclaration(stmt) || stmt.name.text !== 'Events') continue - for (const member of stmt.members) { - if (!ts.isMethodSignature(member)) continue - const name = ts.isStringLiteral(member.name) ? member.name.text : member.name.getText(sf) - const signature = memberSignature(member, sf) - const raw = rawJsDoc(text, member) - const { doc, mode } = parseJsDoc(raw) - const src = pointer(rel, sf, member) - const where = `event '${name}' (${src})` - if (!mode) { - violations.push(`${where} is missing an @mode tag. Add '@mode emit|waterfall|parallel|serial' to its JSDoc (see AGENTS.md).`) - } - // Conclusive structural check: a trailing `next: () => …` parameter is a - // waterfall. (emit vs parallel vs serial is not structurally - // distinguishable, so it is trusted from the tag.) - const last = member.parameters.at(-1) - const hasNext = !!last && last.name.getText(sf) === 'next' - if (mode && hasNext && mode !== 'waterfall') { - violations.push(`${where} has a trailing 'next' parameter (structurally a waterfall) but is tagged '@mode ${mode}'. Fix the tag or the signature.`) - } - if (mode && !hasNext && mode === 'waterfall') { - violations.push(`${where} is tagged '@mode waterfall' but has no trailing 'next' parameter. A waterfall delegates via next().`) - } - if (!doc) violations.push(`${where} has no description prose. Say what happened / what a listener may do, above the block tags.`) - // Payload parameters need a non-empty @param. The `this` receiver is not - // payload, and a waterfall's trailing `next` is covered by its mode. - const { params } = parseTags(raw) - checkParams(where, 'event', member.parameters, params, sf, - p => (ts.isIdentifier(p.name) && p.name.text === 'this') || (hasNext && p === last), violations) - if (mode) entries.push({ name, scope: name.split('/')[0] ?? name, signature, mode, doc, source: src }) + for (const { name, member } of eventMembers(body, sf)) { + const signature = memberSignature(member, sf) + const raw = rawJsDoc(text, member) + const { doc, mode } = parseJsDoc(raw) + const src = pointer(rel, sf, member) + const where = `event '${name}' (${src})` + if (!mode) { + violations.push(`${where} is missing an @mode tag. Add '@mode emit|waterfall|parallel|serial' to its JSDoc (see AGENTS.md).`) } + // Conclusive structural check: a trailing `next: () => …` parameter is a + // waterfall. (emit vs parallel vs serial is not structurally + // distinguishable, so it is trusted from the tag.) + const last = member.parameters.at(-1) + const hasNext = !!last && last.name.getText(sf) === 'next' + if (mode && hasNext && mode !== 'waterfall') { + violations.push(`${where} has a trailing 'next' parameter (structurally a waterfall) but is tagged '@mode ${mode}'. Fix the tag or the signature.`) + } + if (mode && !hasNext && mode === 'waterfall') { + violations.push(`${where} is tagged '@mode waterfall' but has no trailing 'next' parameter. A waterfall delegates via next().`) + } + if (!doc) violations.push(`${where} has no description prose. Say what happened / what a listener may do, above the block tags.`) + // Payload parameters need a non-empty @param. The `this` receiver is not + // payload, and a waterfall's trailing `next` is covered by its mode. + const { params } = parseTags(raw) + checkParams(where, 'event', member.parameters, params, sf, + p => (ts.isIdentifier(p.name) && p.name.text === 'this') || (hasNext && p === last), violations) + if (mode) entries.push({ name, scope: name.split('/')[0] ?? name, signature, mode, doc, source: src }) } } reportViolations('gen-cordis-catalog', violations) @@ -188,26 +177,8 @@ export function collectServices(scanRoot: string = root): ServiceEntry[] { const sf = ts.createSourceFile(abs, text, ts.ScriptTarget.Latest, true) const body = cordisModuleBody(sf) if (!body) continue - // The ctx key → type mapping(s) declared in this file's interface Context. - const keyToType = new Map() - for (const stmt of body.statements) { - if (!ts.isInterfaceDeclaration(stmt) || stmt.name.text !== 'Context') continue - for (const member of stmt.members) { - if (!ts.isPropertySignature(member) || !member.type) continue - const key = member.name.getText(sf) - keyToType.set(key, member.type.getText(sf)) - } - } - if (keyToType.size === 0) continue - // Find each service class declared in the same file and emit an entry. - for (const [key, type] of keyToType) { - const cls = sf.statements.find( - (s): s is ts.ClassDeclaration => ts.isClassDeclaration(s) && s.name?.text === type, - ) - if (!cls) continue // a Pick-mixin member (e.g. timer helpers), not a class here - const abstract = cls.modifiers?.some(m => m.kind === ts.SyntaxKind.AbstractKeyword) ?? false - const clsDoc = parseJsDoc(rawJsDoc(text, cls)).doc - if (!clsDoc) violations.push(`service ctx.${key} (${pointer(rel, sf, cls)}): class ${type} has no JSDoc.`) + // Resolve each ctx key to its service class (shared walk) and emit an entry. + for (const { key, type, cls, abstract, doc: clsDoc } of serviceClasses(body, sf, rel, violations)) { const methods: string[] = [] for (const member of cls.members) { if (!ts.isMethodDeclaration(member)) continue @@ -258,14 +229,14 @@ export function collectServices(scanRoot: string = root): ServiceEntry[] { * sibling check is N/A; keep them current on a vendor bump. */ const INHERITED_EVENTS: InheritedEntry[] = [ - { name: 'internal/plugin', summary: 'A plugin fiber was created.', source: 'vendor/cordis/src/events.ts:197' }, - { name: 'internal/status', summary: 'A fiber changed lifecycle state.', source: 'vendor/cordis/src/events.ts:198' }, - { name: 'internal/service', summary: 'Interception hook for a service binding (no core producer).', source: 'vendor/cordis/src/events.ts:199' }, - { name: 'internal/update', summary: 'Waterfall: a fiber config update is being applied.', source: 'vendor/cordis/src/events.ts:200' }, - { name: 'internal/get', summary: 'Waterfall: a service is being read from the store.', source: 'vendor/cordis/src/events.ts:201' }, - { name: 'internal/set', summary: 'Waterfall: a service is being written to the store.', source: 'vendor/cordis/src/events.ts:202' }, - { name: 'internal/listener', summary: 'A listener was registered.', source: 'vendor/cordis/src/events.ts:203' }, - { name: 'internal/dispatch', summary: 'An event is being dispatched to listeners.', source: 'vendor/cordis/src/events.ts:204' }, + { name: 'internal/plugin', summary: 'A plugin fiber was created.', source: 'vendor/cordis/src/events.ts:328' }, + { name: 'internal/status', summary: 'A fiber changed lifecycle state.', source: 'vendor/cordis/src/events.ts:330' }, + { name: 'internal/service', summary: 'Interception hook for a service binding (no core producer).', source: 'vendor/cordis/src/events.ts:332' }, + { name: 'internal/update', summary: 'Waterfall: a fiber config update is being applied.', source: 'vendor/cordis/src/events.ts:334' }, + { name: 'internal/get', summary: 'Waterfall: a service is being read from the store.', source: 'vendor/cordis/src/events.ts:336' }, + { name: 'internal/set', summary: 'Waterfall: a service is being written to the store.', source: 'vendor/cordis/src/events.ts:338' }, + { name: 'internal/listener', summary: 'A listener was registered.', source: 'vendor/cordis/src/events.ts:340' }, + { name: 'internal/dispatch', summary: 'An event is being dispatched to listeners.', source: 'vendor/cordis/src/events.ts:342' }, { name: 'hmr/change', summary: 'A watched source file changed on disk.', source: 'vendor/hmr/src/index.ts:20' }, { name: 'hmr/reload', summary: 'Plugins are being reloaded after a change.', source: 'vendor/hmr/src/index.ts:21' }, { name: 'exit', summary: 'The process is exiting on a signal.', source: 'vendor/loader/src/index.ts:23' }, @@ -276,12 +247,12 @@ const INHERITED_EVENTS: InheritedEntry[] = [ ] export const INHERITED_SERVICES: InheritedEntry[] = [ - { name: 'ctx.on / ctx.once', summary: 'Register an event listener (disposable).', source: 'vendor/cordis/src/events.ts:29' }, - { name: 'ctx.emit / ctx.parallel / ctx.serial / ctx.bail / ctx.waterfall', summary: 'Dispatch an event (sync / awaited / first-bail / veto-chain).', source: 'vendor/cordis/src/events.ts:29' }, - { name: 'ctx.plugin / ctx.inject', summary: 'Load a plugin / declare required services.', source: 'vendor/cordis/src/registry.ts:144' }, + { name: 'ctx.on / ctx.once', summary: 'Register an event listener (disposable).', source: 'vendor/cordis/src/events.ts:34' }, + { name: 'ctx.emit / ctx.parallel / ctx.serial / ctx.bail / ctx.waterfall', summary: 'Dispatch an event (sync / awaited / first-bail / veto-chain).', source: 'vendor/cordis/src/events.ts:34' }, + { name: 'ctx.plugin / ctx.inject', summary: 'Load a plugin / declare required services.', source: 'vendor/cordis/src/registry.ts:164' }, { name: 'ctx.effect', summary: 'Register a disposable side effect tied to the fiber.', source: 'vendor/cordis/src/fiber.ts:9' }, { name: 'ctx.get / ctx.set / ctx.provide / ctx.accessor / ctx.mixin', summary: 'Low-level service-store access and binding.', source: 'vendor/cordis/src/reflect.ts:7' }, - { name: 'ctx.extend / ctx.isolate / ctx.intercept', summary: 'Derive a child context (scoped services / isolation / interception).', source: 'vendor/cordis/src/context.ts:35' }, + { name: 'ctx.extend / ctx.isolate / ctx.intercept', summary: 'Derive a child context (scoped services / isolation / interception).', source: 'vendor/cordis/src/context.ts:42' }, { name: 'ctx.root / ctx.scope / ctx.fiber / ctx.registry / ctx.reflect / ctx.events / ctx.logger', summary: 'Ambient handles onto the running context graph.', source: 'vendor/cordis/src/context.ts:16' }, { name: 'ctx.timer (+ interval / timeout / throttle / debounce / setTimeout / setInterval)', summary: 'Disposable timer helpers. The `timer` key is provided at runtime; the six helpers are mixed onto ctx directly (declared via Pick).', source: 'vendor/timer/src/index.ts:4' }, { name: 'ctx.loader', summary: 'The config Loader that booted the app (present under the loader).', source: 'vendor/loader/src/index.ts:30' }, diff --git a/scripts/gen-website-api.ts b/scripts/gen-website-api.ts new file mode 100644 index 0000000000..0420ad2ff3 --- /dev/null +++ b/scripts/gen-website-api.ts @@ -0,0 +1,721 @@ +/** + * Generate (and verify) the website API reference under `website/zh-CN/api/`. + * + * The website's API section is FULLY GENERATED from source — never hand-edit + * it. The hand-written hub `api/index.md` sits OUTSIDE the generated subdirs + * (`api/cordis/`, `api/harness/`), so the orphan sweep never touches it. Two tiers: + * + * - `api/cordis/*` — the vendored cordis framework surface (Context, Events, + * Fiber, Registry, Service), driven by the CORDIS_PAGES manifest below. + * Members come from the real class declarations and the `declare module + * './context.ts'` interface merges (the typed `ctx.*` surface a plugin + * author actually sees). + * - `api/harness/*` — one page per `ctx.` harness service (walked from + * every `declare module 'cordis'` Context merge under `packages///src`), + * plus `events.md` listing every harness event grouped by scope. + * + * Prose comes from the JSDoc; the generator HARD-ERRORS (aggregated) when a + * rendered member lacks a summary, a parameter lacks `@param`, or a non-void + * annotated return lacks `@returns` — so a vendor sync or a new service method + * cannot land undocumented without CI going red. Pages are English (the + * planned zh translation flow arrives separately; see docs/i18n/README.md). + * + * Signature fences use the ` ```ts website-api ` info string: doc-typecheck + * only processes its known info strings, so these bare (non-compilable) + * signature fragments are skipped there, while VitePress still highlights the + * `ts` token. The sidebar fragment `website/.vitepress/config/api-sidebar.json` + * is generated alongside so navigation can never drift from the page set. + * + * `tsx scripts/gen-website-api.ts` → write pages + sidebar + * `tsx scripts/gen-website-api.ts --check` → exit 1 if committed copies are + * stale (doc-sync / CI gate) + */ + +import { globSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs' +import { dirname, resolve } from 'node:path' +import ts from 'typescript' +import { checkParams, checkReturns, parseJsDoc, parseTags, pointer, rawJsDoc, reportViolations, type Mode } from './jsdoc.ts' +import { cordisModuleBody, eventMembers, serviceClasses } from './cordis-walk.ts' + +const root = resolve(import.meta.dirname, '..') + +/** Output roots: generated pages and the generated sidebar fragment. */ +const PAGES_DIR = 'website/zh-CN/api' +const SIDEBAR_OUT = 'website/.vitepress/config/api-sidebar.json' + +/** GitHub blob base for source links on the public site (repo-relative paths + * do not resolve on the built site, unlike the in-repo catalogs). */ +const GITHUB = 'https://github.com/deepseek-harness/deepseek-harness/blob/master' + +/** Signature-fence info string (skipped by doc-typecheck, highlighted as ts). */ +const FENCE = 'ts website-api' + +/** Return sorted repository-relative glob matches with stable URL separators. */ +function repoGlob(pattern: string): string[] { + return globSync(pattern, { cwd: root }).map(rel => rel.replaceAll('\\', '/')).sort() +} + +/** One rendered member: a method/property plus its parsed JSDoc. */ +interface MemberDoc { + /** Display name, e.g. `on` or `agent/pre-step`. */ + name: string + /** Heading suffix with parameter names, e.g. `(name, listener, options?)`; + * empty for properties. */ + heading: string + /** All overload signature lines (bodies stripped). */ + signatures: string[] + /** Description prose, one paragraph per line. */ + doc: string + /** Parameter name → `@param` text, in declaration order. */ + params: { name: string; text: string }[] + /** `@returns` text, or null for void/undocumented. */ + returns: string | null + /** Repo-relative `file:line` of the (first) declaration. */ + source: string +} + +/** A cordis-page section: which declarations it renders. */ +type Section = + | { kind: 'class'; file: string; symbol: string; prefix?: string; heading?: string } + | { kind: 'context-merge'; file: string; heading?: string } + | { kind: 'decl'; file: string; symbol: string } + +/** One generated cordis page. */ +interface CordisPage { + out: string + title: string + intro: string + sections: Section[] +} + +/** + * The cordis tier manifest. Deliberately explicit (not a blind walk): the + * vendor `Context` mixes true plugin-author surface with internals, and page + * grouping is an editorial choice — but every member listed here is still + * EXTRACTED, never transcribed, so signatures and docs cannot drift. + */ +const CORDIS_PAGES: CordisPage[] = [ + { + out: 'cordis/context.md', + title: 'Context', + intro: 'The context is the core cordis object: every service, event, and lifecycle API is reached through `ctx`. Event methods (`ctx.on`, `ctx.emit`, …) are documented on [Events](./events.md); `ctx.effect` and `ctx.fiber` on [Fiber](./fiber.md); `ctx.plugin` and `ctx.inject` on [Registry](./registry.md).', + sections: [ + { kind: 'class', file: 'vendor/cordis/src/context.ts', symbol: 'Context', prefix: 'ctx.' }, + { kind: 'context-merge', file: 'vendor/cordis/src/reflect.ts', heading: 'Service store and mixins' }, + ], + }, + { + out: 'cordis/events.md', + title: 'Events', + intro: 'The event system mixed into every context. Harness-defined events are cataloged on [Harness events](../harness/events.md).', + sections: [ + { kind: 'context-merge', file: 'vendor/cordis/src/events.ts' }, + { kind: 'decl', file: 'vendor/cordis/src/events.ts', symbol: 'EventOptions' }, + { kind: 'decl', file: 'vendor/cordis/src/events.ts', symbol: 'DispatchMode' }, + ], + }, + { + out: 'cordis/fiber.md', + title: 'Fiber', + intro: 'A fiber is one loaded plugin instance: its lifecycle state, validated config, and registered effects. `ctx.fiber` is the current fiber; `ctx.effect()` delegates to it.', + sections: [ + { kind: 'context-merge', file: 'vendor/cordis/src/fiber.ts' }, + { kind: 'class', file: 'vendor/cordis/src/fiber.ts', symbol: 'Fiber', heading: 'The Fiber class' }, + { kind: 'decl', file: 'vendor/cordis/src/fiber.ts', symbol: 'Effect' }, + { kind: 'decl', file: 'vendor/cordis/src/fiber.ts', symbol: 'Disposable' }, + { kind: 'decl', file: 'vendor/cordis/src/fiber.ts', symbol: 'EffectMeta' }, + { kind: 'decl', file: 'vendor/cordis/src/fiber.ts', symbol: 'CordisError' }, + { kind: 'decl', file: 'vendor/cordis/src/fiber.ts', symbol: 'ValidationError' }, + ], + }, + { + out: 'cordis/registry.md', + title: 'Registry', + intro: 'Plugin loading and dependency injection.', + sections: [ + { kind: 'context-merge', file: 'vendor/cordis/src/registry.ts' }, + { kind: 'decl', file: 'vendor/cordis/src/registry.ts', symbol: 'Plugin' }, + { kind: 'decl', file: 'vendor/cordis/src/registry.ts', symbol: 'Inject' }, + ], + }, + { + out: 'cordis/service.md', + title: 'Service', + intro: 'Base class for context services: subclass it and load the subclass as a plugin to register `ctx.`.', + sections: [ + { kind: 'class', file: 'vendor/cordis/src/service.ts', symbol: 'Service' }, + ], + }, +] +// --------------------------------------------------------------------------- +// Extraction +// --------------------------------------------------------------------------- + +const sfCache = new Map() + +/** Parse (and cache) one repo-relative source file. */ +function load(rel: string): { sf: ts.SourceFile; text: string } { + const cached = sfCache.get(rel) + if (cached) return cached + const text = readFileSync(resolve(root, rel), 'utf8') + const sf = ts.createSourceFile(rel, text, ts.ScriptTarget.Latest, true) + const entry = { sf, text } + sfCache.set(rel, entry) + return entry +} +// The module-merge walk (cordisModuleBody / eventMembers / serviceClasses) is +// shared with gen-cordis-catalog.ts via cordis-walk.ts. + +/** Signature text of a member: full text minus body/initializer, whitespace + * collapsed, trailing semicolon stripped. */ +function signatureOf(member: ts.Node, sf: ts.SourceFile): string { + const full = member.getText(sf) + const tail = (member as { body?: ts.Node; initializer?: ts.Node }).body + ?? (member as { initializer?: ts.Node }).initializer + const sig = tail ? full.slice(0, full.length - tail.getText(sf).length).replace(/[=\s]+$/, '') : full + return sig.replace(/\s*;?\s*$/, '').replace(/\s+/g, ' ').trim() +} + +/** `(a, b?, ...rest)` heading suffix from a parameter list, `this` dropped. */ +function headingParams(parameters: readonly ts.ParameterDeclaration[], sf: ts.SourceFile): string { + const names = parameters + .filter(p => !(ts.isIdentifier(p.name) && p.name.text === 'this')) + .map((p) => { + const dots = p.dotDotDotToken ? '...' : '' + const opt = p.questionToken || p.initializer ? '?' : '' + return `${dots}${p.name.getText(sf)}${opt}` + }) + return `(${names.join(', ')})` +} + +/** Whether a class member is renderable public API (non-static half). */ +function isPublicInstance(member: ts.ClassElement): boolean { + const mods = ts.getCombinedModifierFlags(member) + if (mods & (ts.ModifierFlags.Private | ts.ModifierFlags.Protected | ts.ModifierFlags.Static)) return false + if (!member.name) return false + if (ts.isComputedPropertyName(member.name) || ts.isPrivateIdentifier(member.name)) return false + return !member.name.getText().startsWith('_') +} + +/** Whether a class member is renderable public STATIC API. */ +function isPublicStatic(member: ts.ClassElement): boolean { + const mods = ts.getCombinedModifierFlags(member) + if (mods & (ts.ModifierFlags.Private | ts.ModifierFlags.Protected)) return false + if (!(mods & ts.ModifierFlags.Static)) return false + if (!member.name || ts.isComputedPropertyName(member.name) || ts.isPrivateIdentifier(member.name)) return false + return !member.name.getText().startsWith('_') +} + +/** Build a MemberDoc from a declaration group (overloads share one entry), + * collecting completeness violations for everything rendered. */ +function memberDoc( + where: string, + name: string, + group: (ts.MethodDeclaration | ts.MethodSignature | ts.PropertyDeclaration | ts.PropertySignature | ts.GetAccessorDeclaration)[], + rel: string, + violations: string[], +): MemberDoc { + const { sf, text } = load(rel) + const first = group[0] + if (!first) throw new Error(`gen-website-api: empty member group for ${name}`) + // Doc from the first overload that carries JSDoc prose. + const rawDocs = group.map(m => rawJsDoc(text, m)) + const docIndex = rawDocs.findIndex(r => parseJsDoc(r).doc !== '') + const raw = docIndex === -1 ? '' : (rawDocs[docIndex] ?? '') + const doc = parseJsDoc(raw).doc + if (!doc) violations.push(`${where} has no JSDoc prose.`) + const { params: tags, returns } = parseTags(raw) + const params: { name: string; text: string }[] = [] + let returnsText: string | null = null + const funcLike = group.filter((m): m is ts.MethodDeclaration | ts.MethodSignature => ts.isMethodDeclaration(m) || ts.isMethodSignature(m)) + const docCarrier = funcLike[docIndex === -1 ? 0 : docIndex] + if (docCarrier) { + checkParams(where, 'website-api', docCarrier.parameters, tags, sf, + p => ts.isIdentifier(p.name) && p.name.text === 'this', violations) + if (docCarrier.type) { + checkReturns(where, docCarrier.type, returns, sf, violations) + } else if (!returns && ts.isMethodDeclaration(docCarrier)) { + // Comment-only vendor policy: we cannot add a return type annotation to + // pinned upstream source, so an unannotated rendered method must carry + // an explicit @returns describing the result instead. + violations.push(`${where} has no return type annotation; document the result with @returns.`) + } + for (const p of docCarrier.parameters) { + if (ts.isIdentifier(p.name) && p.name.text === 'this') continue + const pname = p.name.getText(sf) + const tag = tags.get(pname) + if (tag) params.push({ name: pname, text: tag }) + } + returnsText = returns + } + const headingSource = docCarrier ?? funcLike[0] + return { + name, + heading: headingSource ? headingParams(headingSource.parameters, sf) : '', + signatures: (ts.isMethodDeclaration(first) && funcLike.length > 1 + ? funcLike.filter(m => ts.isMethodDeclaration(m) && !m.body) + : group).map(m => signatureOf(m, sf)), + doc, + params, + returns: returnsText, + source: pointer(rel, sf, first), + } +} + +/** Resolve an `extends Pick` heritage clause on the Context + * merge to the named members of `Class` declared in the same file — the fiber + * merge (`interface Context extends Pick`) is the motivating + * case: without this, `ctx.effect` had no documented signature anywhere. */ +function heritageMembers( + stmt: ts.InterfaceDeclaration, + sf: ts.SourceFile, + groups: Map, +): void { + for (const clause of stmt.heritageClauses ?? []) { + for (const type of clause.types) { + if (!ts.isIdentifier(type.expression) || type.expression.text !== 'Pick') continue + const [target, keys] = type.typeArguments ?? [] + if (!target || !keys || !ts.isTypeReferenceNode(target)) continue + const targetName = target.typeName.getText(sf) + const cls = sf.statements.find( + (s): s is ts.ClassDeclaration => ts.isClassDeclaration(s) && s.name?.text === targetName, + ) + if (!cls) continue + const picked = new Set() + const collect = (node: ts.TypeNode): void => { + if (ts.isLiteralTypeNode(node) && ts.isStringLiteral(node.literal)) picked.add(node.literal.text) + if (ts.isUnionTypeNode(node)) node.types.forEach(collect) + } + collect(keys) + for (const member of cls.members) { + if (!ts.isMethodDeclaration(member)) continue + const name = member.name.getText(sf) + if (!picked.has(name)) continue + const group = groups.get(name) ?? [] + group.push(member) + groups.set(name, group) + } + } + } +} + +/** Members of the `interface Context` merge in `rel`, overloads grouped; + * `Pick<…>` heritage resolved to the picked class members. */ +function contextMergeMembers(rel: string, violations: string[]): MemberDoc[] { + const { sf } = load(rel) + const body = cordisModuleBody(sf) + if (!body) throw new Error(`gen-website-api: ${rel} has no context module merge`) + const groups = new Map() + for (const stmt of body.statements) { + if (!ts.isInterfaceDeclaration(stmt) || stmt.name.text !== 'Context') continue + heritageMembers(stmt, sf, groups) + for (const member of stmt.members) { + if (!ts.isMethodSignature(member) && !ts.isPropertySignature(member)) continue + if (ts.isComputedPropertyName(member.name)) continue + const name = member.name.getText(sf) + const group = groups.get(name) ?? [] + group.push(member) + groups.set(name, group) + } + } + return [...groups.entries()].map(([name, group]) => + memberDoc(`ctx.${name} (${rel})`, name, group, rel, violations)) +} + +/** Instance + static members of one class, as two rendered lists. The class's + * same-named top-level interface half (declaration merging — vendor Context + * declares `root`/`events`/`logger`/… on the interface) is folded into the + * instance list, so neither half of a merged symbol goes undocumented. */ +function classMembers(rel: string, className: string, violations: string[]): { + doc: string + instance: MemberDoc[] + statics: MemberDoc[] + source: string +} { + const { sf, text } = load(rel) + const cls = sf.statements.find( + (s): s is ts.ClassDeclaration => ts.isClassDeclaration(s) && s.name?.text === className, + ) + if (!cls) throw new Error(`gen-website-api: class ${className} not found in ${rel}`) + const clsDoc = parseJsDoc(rawJsDoc(text, cls)).doc + if (!clsDoc) violations.push(`class ${className} (${pointer(rel, sf, cls)}) has no JSDoc.`) + type Renderable = ts.MethodDeclaration | ts.PropertyDeclaration | ts.GetAccessorDeclaration | ts.PropertySignature + const instance = new Map() + const statics = new Map() + for (const member of cls.members) { + const renderable = ts.isMethodDeclaration(member) || ts.isPropertyDeclaration(member) || ts.isGetAccessorDeclaration(member) + if (!renderable) continue + const name = member.name.getText(sf) + if (isPublicInstance(member)) { + const group = instance.get(name) ?? [] + group.push(member) + instance.set(name, group) + } else if (isPublicStatic(member) && !ts.isGetAccessorDeclaration(member)) { + const group = statics.get(name) ?? [] + group.push(member) + statics.set(name, group) + } + } + const iface = sf.statements.find( + (s): s is ts.InterfaceDeclaration => ts.isInterfaceDeclaration(s) && s.name.text === className, + ) + for (const member of iface?.members ?? []) { + if (!ts.isPropertySignature(member)) continue + if (ts.isComputedPropertyName(member.name)) continue + const name = member.name.getText(sf) + const group = instance.get(name) ?? [] + group.push(member) + instance.set(name, group) + } + const toDocs = (groups: Map, prefix: string): MemberDoc[] => + [...groups.entries()].map(([name, group]) => + memberDoc(`${prefix}${name} (${rel})`, name, group, rel, violations)) + return { + doc: clsDoc, + instance: toDocs(instance, `${className}#`), + statics: toDocs(statics, `${className}.`), + source: pointer(rel, sf, cls), + } +} + +/** Splice every function-like BODY out of a declaration's text, leaving the + * signature (`) {` → `)`). A reference paste shows shapes, not implementation; + * property initializers (e.g. an `as const` code table) are data and stay. */ +function stripBodies(node: ts.Node, sf: ts.SourceFile): string { + const cuts: { start: number; end: number }[] = [] + const visit = (n: ts.Node): void => { + const funcLike = ts.isMethodDeclaration(n) || ts.isConstructorDeclaration(n) + || ts.isFunctionDeclaration(n) || ts.isGetAccessorDeclaration(n) || ts.isSetAccessorDeclaration(n) + if (funcLike && n.body) { + // Cut from just after the parameter close (or return-type end) through + // the body, so `foo(a: string) { … }` renders as `foo(a: string)`. + const sigEnd = (n.type ?? n.parameters[n.parameters.length - 1] ?? n).getEnd() + // Find the `)` (and optional `: Type`) boundary: body start is exact. + cuts.push({ start: sigEnd, end: n.body.getEnd() }) + return // nothing renderable inside the body + } + n.forEachChild(visit) + } + visit(node) + const base = node.getStart(sf) + let out = node.getText(sf) + for (const cut of cuts.sort((a, b) => b.start - a.start)) { + const head = out.slice(0, cut.start - base) + // Keep everything of the signature up to the closing paren / return type, + // drop ` { … }`. The head may end mid-signature (last param), so retain + // the source between sigEnd and the body's `{` MINUS trailing space. + const between = out.slice(cut.start - base, cut.end - base) + const bodyBrace = between.indexOf('{') + out = head + between.slice(0, bodyBrace).trimEnd() + out.slice(cut.end - base) + } + return out +} + +/** Verbatim declaration paste: every top-level statement named `symbol` + * (class + merged namespace both), with leading JSDoc prose extracted and + * function bodies stripped (a reference shows shapes, not implementation). */ +function declPaste(rel: string, symbol: string): { doc: string; code: string; source: string } { + const { sf, text } = load(rel) + const matches = sf.statements.filter((s) => { + const named = ts.isInterfaceDeclaration(s) || ts.isTypeAliasDeclaration(s) + || ts.isClassDeclaration(s) || ts.isEnumDeclaration(s) || ts.isModuleDeclaration(s) + return named && s.name?.getText(sf) === symbol + }) + if (matches.length === 0) throw new Error(`gen-website-api: declaration ${symbol} not found in ${rel}`) + const first = matches[0] + if (!first) throw new Error(`gen-website-api: declaration ${symbol} not found in ${rel}`) + const doc = parseJsDoc(rawJsDoc(text, first)).doc + const code = matches.map(s => stripBodies(s, sf).replace(/^export\s+(default\s+)?/, '')).join('\n\n') + return { doc, code, source: pointer(rel, sf, first) } +} + +/** One harness service with member-level detail. */ +interface HarnessService { + key: string + type: string + abstract: boolean + doc: string + members: MemberDoc[] + source: string + /** Owning npm package name (from the package.json beside the entry). */ + pkg: string +} + +/** Walk every harness `declare module 'cordis'` Context merge → services. */ +function collectHarnessServices(violations: string[]): HarnessService[] { + const services: HarnessService[] = [] + for (const rel of repoGlob('packages/*/*/src/index.ts')) { + const { sf, text } = load(rel) + if (!text.includes('interface Context')) continue + const body = cordisModuleBody(sf) + if (!body) continue + const pkgJson = resolve(root, dirname(dirname(rel)), 'package.json') + // Manifest shape is repo-owned; `name` is the one field read here. + const manifest = JSON.parse(readFileSync(pkgJson, 'utf8')) as { name: string } + const pkg = manifest.name + for (const { key, type, cls, abstract, doc: clsDoc } of serviceClasses(body, sf, rel, violations)) { + const groups = new Map() + for (const member of cls.members) { + // Public properties are API too: ctx.codeRuntime.language/isolation + // are readonly descriptors consumers key presentation off. + const renderable = ts.isMethodDeclaration(member) || ts.isPropertyDeclaration(member) || ts.isGetAccessorDeclaration(member) + if (!renderable) continue + if (!isPublicInstance(member)) continue + const name = member.name.getText(sf) + const group = groups.get(name) ?? [] + group.push(member) + groups.set(name, group) + } + const members = [...groups.entries()].map(([name, group]) => + memberDoc(`ctx.${key}.${name} (${rel})`, name, group, rel, violations)) + services.push({ key, type, abstract, doc: clsDoc, members, source: pointer(rel, sf, cls), pkg }) + } + } + return services.sort((a, b) => a.key.localeCompare(b.key)) +} + +/** One harness event with member-level detail. */ +interface HarnessEvent { + name: string + scope: string + mode: Mode | null + signature: string + doc: string + params: { name: string; text: string }[] + source: string +} + +/** Walk every harness `interface Events` merge → events. */ +function collectHarnessEvents(violations: string[]): HarnessEvent[] { + const events: HarnessEvent[] = [] + for (const rel of repoGlob('packages/*/*/src/*.ts')) { + const { sf, text } = load(rel) + if (!text.includes('interface Events')) continue + const body = cordisModuleBody(sf) + if (!body) continue + for (const { name, member } of eventMembers(body, sf)) { + const raw = rawJsDoc(text, member) + const { doc, mode } = parseJsDoc(raw) + if (!mode) violations.push(`event '${name}' (${pointer(rel, sf, member)}) is missing @mode.`) + if (!doc) violations.push(`event '${name}' (${pointer(rel, sf, member)}) has no JSDoc prose.`) + const { params: tags } = parseTags(raw) + const last = member.parameters.at(-1) + const hasNext = !!last && last.name.getText(sf) === 'next' + checkParams(`event '${name}' (${pointer(rel, sf, member)})`, 'website-api', member.parameters, tags, sf, + p => (ts.isIdentifier(p.name) && p.name.text === 'this') || (hasNext && p === last), violations) + const params: { name: string; text: string }[] = [] + for (const p of member.parameters) { + const pname = p.name.getText(sf) + const tag = tags.get(pname) + if (tag) params.push({ name: pname, text: tag }) + } + events.push({ name, scope: name.split('/')[0] ?? name, mode, signature: signatureOf(member, sf), doc, params, source: pointer(rel, sf, member) }) + } + } + return events.sort((a, b) => a.name.localeCompare(b.name)) +} + +// --------------------------------------------------------------------------- +// Rendering +// --------------------------------------------------------------------------- + +const BANNER = '' + +/** GitHub source link for a `file:line` pointer. */ +function sourceLink(source: string): string { + const [file, line] = source.split(':') + return `[Source](${GITHUB}/${file}#L${line})` +} + +/** Normalize JSDoc inline `{@link X}` / `{@link X|label}` / `{@link X label}` + * tags to plain Markdown code spans — left verbatim they leak into the built + * page as literal `{@link …}` text. */ +function unlink(text: string): string { + return text.replace(/\{@link\s+([^}|\s]+)\s*(?:[|\s]\s*([^}]*))?\}/g, (_m, target: string, label?: string) => { + const name = label?.trim() + return name && name !== '' ? name : `\`${target}\`` + }) +} + +/** Render prose paragraphs (one per line of `doc`), JSDoc links normalized. */ +function prose(doc: string): string[] { + return unlink(doc).split('\n').filter(l => l.trim() !== '') +} + +/** Render one member section at heading depth 3. */ +function renderMember(prefix: string, m: MemberDoc): string[] { + const lines: string[] = [] + const call = m.heading === '' ? '' : m.heading + lines.push(`### ${prefix}${m.name}${call}`, '') + lines.push('```' + FENCE) + for (const sig of m.signatures) lines.push(sig) + lines.push('```', '') + lines.push(...prose(m.doc), '') + if (m.params.length > 0) { + for (const p of m.params) lines.push(`- \`${p.name}\` — ${unlink(p.text)}`) + lines.push('') + } + if (m.returns) lines.push(`**Returns** ${unlink(m.returns)}`, '') + lines.push(sourceLink(m.source), '') + return lines +} + +/** Render one cordis-tier page from its manifest entry. */ +function renderCordisPage(page: CordisPage, violations: string[]): string { + const lines: string[] = [BANNER, '', `# ${page.title}`, '', page.intro, ''] + for (const section of page.sections) { + if (section.kind !== 'decl' && section.heading) lines.push(`## ${section.heading}`, '') + if (section.kind === 'context-merge') { + for (const m of contextMergeMembers(section.file, violations)) { + lines.push(...renderMember('ctx.', m)) + } + } else if (section.kind === 'class') { + const cls = classMembers(section.file, section.symbol, violations) + lines.push(...prose(cls.doc), '', sourceLink(cls.source), '') + const instancePrefix = section.prefix ?? `${section.symbol.toLowerCase()}.` + for (const m of cls.instance) lines.push(...renderMember(instancePrefix, m)) + if (cls.statics.length > 0) { + lines.push('## Static members', '') + for (const m of cls.statics) lines.push(...renderMember(`${section.symbol}.`, m)) + } + } else { + const decl = declPaste(section.file, section.symbol) + lines.push(`## ${section.symbol}`, '') + if (decl.doc) lines.push(...prose(decl.doc), '') + lines.push('```' + FENCE, decl.code, '```', '', sourceLink(decl.source), '') + } + } + return `${lines.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd()}\n` +} + +/** kebab-case a ctx key: `agentLoop` → `agent-loop`. */ +function kebab(key: string): string { + return key.replace(/[A-Z]/g, c => `-${c.toLowerCase()}`) +} + +/** Render one harness service page. */ +function renderServicePage(svc: HarnessService): string { + const seam = svc.abstract ? ' (abstract seam)' : '' + const lines: string[] = [ + BANNER, '', + `# ctx.${svc.key}`, '', + `\`${svc.type}\`${seam} — provided by \`${svc.pkg}\`.`, '', + ...prose(svc.doc), '', + sourceLink(svc.source), '', + ] + for (const m of svc.members) lines.push(...renderMember(`ctx.${svc.key}.`, m)) + return `${lines.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd()}\n` +} + +/** Render the harness events page, grouped by scope. */ +function renderEventsPage(events: HarnessEvent[]): string { + const lines: string[] = [ + BANNER, '', + '# Harness events', '', + `Every event the harness packages declare on the cordis event bus (${events.length} total), grouped by scope. The **mode** is the dispatch semantics (\`emit\` fire-and-forget, \`parallel\` awaited, \`serial\` first-bail, \`waterfall\` veto-chain — a waterfall listener MUST call \`next()\` to delegate).`, '', + ] + const scopes = [...new Set(events.map(e => e.scope))].sort() + for (const scope of scopes) { + lines.push(`## ${scope}/*`, '') + for (const e of events.filter(ev => ev.scope === scope)) { + lines.push(`### ${e.name}`, '') + lines.push(`**Mode:** \`${e.mode ?? 'unknown'}\``, '') + lines.push('```' + FENCE, e.signature, '```', '') + lines.push(...prose(e.doc), '') + if (e.params.length > 0) { + for (const p of e.params) lines.push(`- \`${p.name}\` — ${unlink(p.text)}`) + lines.push('') + } + lines.push(sourceLink(e.source), '') + } + } + return `${lines.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd()}\n` +} + +// --------------------------------------------------------------------------- +// Assembly + CLI +// --------------------------------------------------------------------------- + +/** Build every generated file as `relPath → content`. */ +export function generate(): Map { + const violations: string[] = [] + const files = new Map() + + for (const page of CORDIS_PAGES) { + files.set(`${PAGES_DIR}/${page.out}`, renderCordisPage(page, violations)) + } + + const services = collectHarnessServices(violations) + for (const svc of services) { + files.set(`${PAGES_DIR}/harness/${kebab(svc.key)}.md`, renderServicePage(svc)) + } + + const events = collectHarnessEvents(violations) + files.set(`${PAGES_DIR}/harness/events.md`, renderEventsPage(events)) + + reportViolations('gen-website-api', violations) + + const sidebar = { + cordis: CORDIS_PAGES.map(p => ({ + text: p.title, + link: `/zh-CN/api/${p.out.replace(/\.md$/, '')}`, + })), + harness: [ + ...services.map(s => ({ text: `ctx.${s.key}`, link: `/zh-CN/api/harness/${kebab(s.key)}` })), + { text: 'Events', link: '/zh-CN/api/harness/events' }, + ], + } + files.set(SIDEBAR_OUT, `${JSON.stringify(sidebar, null, 2)}\n`) + return files +} + +/** CLI entry: default writes, `--check` fails on stale/orphan files. Guarded + * behind an entry-point check so tests can import `generate()`. */ +function main(): void { + const check = process.argv.includes('--check') + const files = generate() + + // Orphan detection: a generated-dir page that generate() no longer emits + // (e.g. a service was renamed) must be deleted, not left to rot. + const expected = new Set([...files.keys()]) + // Orphans live in the generated subdirs only; the hand-written api/index.md + // is one level up and never matches this glob. + const onDisk = repoGlob(`${PAGES_DIR}/{cordis,harness}/*.md`) + const orphans = onDisk.filter(rel => !expected.has(rel)) + + if (check) { + const stale: string[] = [] + for (const [rel, content] of files) { + let current: string | null = null + try { + current = readFileSync(resolve(root, rel), 'utf8') + } catch { + // Missing file: reported as stale below; readFileSync is the probe. + } + if (current !== content) stale.push(rel) + } + if (stale.length > 0 || orphans.length > 0) { + console.error('gen-website-api: website API reference is stale. Run `pnpm run gen-website-api` and commit the result.') + for (const rel of stale) console.error(` stale: ${rel}`) + for (const rel of orphans) console.error(` orphan (delete): ${rel}`) + process.exit(1) + } + console.log(`gen-website-api: ${files.size} generated file(s) fresh.`) + return + } + + for (const [rel, content] of files) { + const abs = resolve(root, rel) + mkdirSync(dirname(abs), { recursive: true }) + writeFileSync(abs, content) + } + for (const rel of orphans) { + console.log(`gen-website-api: orphan page ${rel} — delete it (no longer generated).`) + } + console.log(`gen-website-api: wrote ${files.size} file(s).`) +} + +// Run only when invoked as a script, not when imported by a test. +if (process.argv[1] && import.meta.filename === resolve(process.argv[1])) { + main() +} diff --git a/scripts/md-fences.ts b/scripts/md-fences.ts new file mode 100644 index 0000000000..8c84b27457 --- /dev/null +++ b/scripts/md-fences.ts @@ -0,0 +1,55 @@ +/** + * Shared fenced-code-block extractor for the Markdown doc gates + * (`doc-typecheck.ts`, `verify-website-yaml.ts`). One scanner, per-gate + * classification: each gate maps a fence info string (` ```ts `, + * ` ```yaml ignore-check `, …) to its own kind tag and receives every + * classified block with its 1-based opening-fence line. + */ + +import { readFileSync } from 'node:fs' + +/** One extracted fenced block, classified by the caller's `classify`. */ +export interface Fence { + /** 1-based line of the opening fence. */ + line: number + kind: K + code: string +} + +/** + * Extract every fenced block of `absPath` whose info string `classify` maps + * to a kind. Blocks classified `null` are skipped (their bodies are still + * consumed, so an unrelated fence can never leak into a tracked one). + * + * @param absPath — absolute path of the Markdown file. + * @param classify — info string (trimmed, e.g. `ts ignore-check`) → kind, or + * null for fences this gate does not track. + * @returns the classified blocks in document order. + */ +export function extractFences(absPath: string, classify: (info: string) => K | null): Fence[] { + const lines = readFileSync(absPath, 'utf8').split('\n') + const blocks: Fence[] = [] + let open: { line: number; kind: K; body: string[] } | null = null + let skipping = false + + lines.forEach((raw, i) => { + const fence = /^```(\s*)(\S.*)?$/.exec(raw) + if (!fence) { + if (open) open.body.push(raw) + return + } + if (open) { + blocks.push({ line: open.line, kind: open.kind, code: open.body.join('\n') }) + open = null + return + } + if (skipping) { + skipping = false + return + } + const kind = classify((fence[2] ?? '').trim()) + if (kind !== null) open = { line: i + 1, kind, body: [] } + else skipping = true + }) + return blocks +} diff --git a/scripts/run-gates.ts b/scripts/run-gates.ts index f2bea88cca..fde83ff5ea 100644 --- a/scripts/run-gates.ts +++ b/scripts/run-gates.ts @@ -212,6 +212,7 @@ function ciPrimaryGates(): Gate[] { ...docSyncLeafGates(), pnpmScript('module-graph', 'verify-module-graph', { label: 'module graph' }), pnpmScript('knip', 'knip'), + pnpmScript('website-build', 'website:build', { label: 'website build' }), pnpmScript('build', 'build', { needs: ['typecheck'] }), pnpmScript('publint', 'publint', { needs: ['build'] }), pnpmScript('node-next-types', 'verify-node-next-types', { @@ -231,6 +232,7 @@ function ciStaticGates(): Gate[] { ...docSyncLeafGates(), pnpmScript('module-graph', 'verify-module-graph', { label: 'module graph' }), pnpmScript('knip', 'knip'), + pnpmScript('website-build', 'website:build', { label: 'website build' }), ] } @@ -321,6 +323,7 @@ function docSyncLeafGates(options: { pnpmScript('persistence-catalog', 'verify-persistence-catalog', { label: 'persistence catalog' }), pnpmScript('doc-graphs', 'verify-doc-graphs', { label: 'doc graphs' }), pnpmScript('scoped-events', 'verify-scoped-events', { label: 'scoped events' }), + pnpmScript('website-api', 'verify-website-api', { label: 'website api' }), pnpmScript('markdown-wrap', 'verify-md-wrap', { label: 'markdown wrap' }), pnpmScript('markdown-links', 'verify-md-links', { label: 'markdown links' }), pnpmScript('doc-refs', 'verify-doc-refs', { label: 'doc refs' }), @@ -334,6 +337,7 @@ function docSyncLeafGates(options: { pnpmScript('translation-pairing', 'verify-translation-pairing', { label: 'translation pairing' }), pnpmScript('doc-budgets', 'verify-doc-budgets', { label: 'doc budgets' }), pnpmScript('package-readme-limitations', 'verify-package-readme-limitations', { label: 'package README limitations' }), + pnpmScript('website-yaml', 'verify-website-yaml', { label: 'website yaml' }), ] } diff --git a/scripts/verify-type-equiv.ts b/scripts/verify-type-equiv.ts index 3e2b6f568b..08278a6a8f 100644 --- a/scripts/verify-type-equiv.ts +++ b/scripts/verify-type-equiv.ts @@ -11,7 +11,7 @@ import ts from 'typescript' const root = resolve(import.meta.dirname, '..') /** Scan doc-typecheck's full Markdown scope so unmanifested blocks also fail. */ -const MARKDOWN_GLOBS = ['README.md', 'docs/**/*.md', 'packages/*/*.md', 'packages/*/*/*.md'] +const MARKDOWN_GLOBS = ['README.md', 'docs/**/*.md', 'packages/*/*.md', 'packages/*/*/*.md', 'website/zh-CN/**/*.md'] /** One manifest entry: a documented type-equiv block and its source symbol. */ interface ManifestEntry { diff --git a/scripts/verify-website-yaml.ts b/scripts/verify-website-yaml.ts new file mode 100644 index 0000000000..6c086ab645 --- /dev/null +++ b/scripts/verify-website-yaml.ts @@ -0,0 +1,269 @@ +/** + * Doc-sync gate: verify the fenced ```yaml examples in the website against + * the loader and the workspace truth. A `cordis.yml` example that names a + * plugin that does not exist, or passes a config key the plugin never + * declared, is worse than no example — it fails silently for the reader. + * + * Scope: `website/zh-CN/**​/*.md`, EXCLUDING `website/zh-CN/api/**` (the api + * pages are generator-owned — their yaml examples are verified at generation + * time by a later stream, not re-checked here). Blocks opt out with + * ` ```yaml ignore-check ` (same philosophy as doc-typecheck's opt-out: the + * count is reported, an unchecked block is a visible decision, not a silent + * hole — placeholder plugin names in tutorials are the legitimate case). + * + * Each checked block is parsed with the loader's REAL schema — + * `JSON_SCHEMA` extended with the `!!js` scalar type exactly as + * vendor/include/src/index.ts declares it — so `!!js process.env.X` parses + * here iff it parses at runtime. Then: + * + * - Root is an ARRAY → a cordis.yml entry list. Every item must be a mapping + * with a string `name` and only the keys `EntryOptions` declares + * (vendor/loader/src/config/entry.ts plus the isolate.ts merge: + * id, name, config, group, disabled, inject, intercept, isolate). + * - `./` / `../` names are illustrative local plugins — existence is not + * checkable, skip. `group:*` names are loader built-ins; their `config` + * is itself an entry list and is recursed into. + * - Any other name must be a real workspace package (`packages/*​/*` and + * `vendor/*` package.json names). + * - For `@deepseek-ai/dsh-*` names the config-catalog generator is the + * truth: kind `config` → the yaml `config`'s top-level keys must be + * properties of the declared config type (member names of the first + * catalog paste ∪ top-level segments of the runtime schema keys); + * config-free kinds → a non-empty `config` mapping is a violation; + * seam/library kinds → name existence only (loading one directly is + * dubious, but that is a docs-prose concern, not this gate's). + * - Root is a MAPPING or scalar → a fragment (e.g. a bare `config:` excerpt): + * syntax check only. + * + * This is a checker, not a fixer: it reports `file:line message` and exits 1. + * + * Run: `tsx scripts/verify-website-yaml.ts`. + */ + +import { globSync, readFileSync } from 'node:fs' +import { resolve } from 'node:path' +import * as yaml from 'js-yaml' +import ts from 'typescript' +import { collectConfigCatalog, type CatalogEntry } from './gen-config-catalog.ts' +import { extractFences } from './md-fences.ts' + +const root = resolve(import.meta.dirname, '..') + +/** Mirror of the loader's yaml schema (vendor/include/src/index.ts): the + * `!!js` tag parses to an expression wrapper, everything else is JSON. */ +const JsExpr = new yaml.Type('tag:yaml.org,2002:js', { + kind: 'scalar', + resolve: data => typeof data === 'string', + construct: (data: string) => ({ __jsExpr: data }), +}) +const schema = yaml.JSON_SCHEMA.extend(JsExpr) + +/** The exact key set an entry mapping may carry: `EntryOptions` in + * vendor/loader/src/config/entry.ts plus the isolate.ts interface merge. */ +const ENTRY_KEYS = ['id', 'name', 'config', 'group', 'disabled', 'inject', 'intercept', 'isolate'] as const + +/** One `file:line message` finding. */ +interface Violation { + file: string + /** 1-based line of the block's opening fence. */ + line: number + message: string +} + +/** One extracted ```yaml block. */ +interface Block { + file: string + /** 1-based line of the opening fence. */ + line: number + kind: 'check' | 'ignore' + code: string +} + +/** Extract every ```yaml / ```yaml ignore-check block from one Markdown file. */ +function extractBlocks(file: string): Block[] { + return extractFences(resolve(root, file), info => + info === 'yaml' ? 'check' : info === 'yaml ignore-check' ? 'ignore' : null) + .map(f => ({ file, line: f.line, kind: f.kind, code: f.code })) +} + +/** Every workspace package name: `packages//` and `vendor/`. */ +function knownPackages(): Set { + const names = new Set() + for (const pattern of ['packages/*/*/package.json', 'vendor/*/package.json']) { + for (const match of globSync(pattern, { cwd: root })) { + const pkg: unknown = JSON.parse(readFileSync(resolve(root, match), 'utf8')) + if (typeof pkg === 'object' && pkg !== null && 'name' in pkg && typeof pkg.name === 'string') { + names.add(pkg.name) + } + } + } + return names +} + +/** The catalog, built once on first `@deepseek-ai/dsh-*` name, keyed by pkg. */ +let catalogByPkg: Map | null = null +function catalogFor(pkg: string): CatalogEntry | undefined { + catalogByPkg ??= new Map(collectConfigCatalog().map(e => [e.pkg, e])) + return catalogByPkg.get(pkg) +} + +/** Top-level property names of the first catalog paste (the verbatim config + * type declaration), parsed as source text. */ +function pasteKeys(paste: string): Set { + const sf = ts.createSourceFile('paste.ts', paste, ts.ScriptTarget.Latest, true) + const keys = new Set() + const addMembers = (members: ts.NodeArray): void => { + for (const m of members) { + if (ts.isPropertySignature(m) || ts.isMethodSignature(m)) { + const name = m.name + keys.add(ts.isIdentifier(name) || ts.isStringLiteral(name) ? name.text : name.getText(sf)) + } + } + } + for (const stmt of sf.statements) { + if (ts.isInterfaceDeclaration(stmt)) addMembers(stmt.members) + else if (ts.isTypeAliasDeclaration(stmt) && ts.isTypeLiteralNode(stmt.type)) addMembers(stmt.type.members) + } + return keys +} + +/** The allowed top-level config keys of a kind-`config` catalog entry: the + * first paste's member names ∪ the schema keys' top-level segments + * (`agents[].id` → `agents`). Cached per entry. */ +const allowedKeysCache = new Map>() +function allowedConfigKeys(entry: CatalogEntry): Set { + const cached = allowedKeysCache.get(entry.pkg) + if (cached) return cached + const keys = pasteKeys(entry.pastes?.[0]?.text ?? '') + for (const path of entry.schemaKeys ?? []) { + const top = path.split('.')[0]?.replace(/\[\]$/, '') + if (top) keys.add(top) + } + allowedKeysCache.set(entry.pkg, keys) + return keys +} + +/** A parsed yaml mapping (arrays and `!!js` wrappers excluded). */ +function asMapping(value: unknown): Record | null { + if (typeof value !== 'object' || value === null || Array.isArray(value)) return null + if ('__jsExpr' in value) return null + return value as Record +} + +/** Check one cordis.yml entry list (recursing into `group:` sub-lists). */ +function checkEntryList( + items: unknown[], + known: Set, + block: Block, + violations: Violation[], +): void { + const flag = (message: string): void => { + violations.push({ file: block.file, line: block.line, message }) + } + items.forEach((item, index) => { + const at = `entry ${index + 1}` + const entry = asMapping(item) + if (!entry) { + flag(`${at}: not a mapping`) + return + } + const name = entry['name'] + if (typeof name !== 'string') { + flag(`${at}: missing string \`name\``) + return + } + for (const key of Object.keys(entry)) { + if (!(ENTRY_KEYS as readonly string[]).includes(key)) { + flag(`${at} (${name}): unknown entry key \`${key}\` (EntryOptions allows: ${[...ENTRY_KEYS].join(', ')})`) + } + } + // Illustrative local plugin — nothing on disk to check against. + if (name.startsWith('./') || name.startsWith('../')) return + // A `group:`-style pseudo-name is NOT loadable: tree.import() only + // special-cases the `cordis:` prefix, and nothing in this repo registers + // loader builtins — reject it and point at the real group plugin. + if (name.startsWith('group:')) { + flag(`${at}: \`${name}\` is not loadable (no loader builtin is registered); use \`@cordisjs/plugin-group\` with \`group: true\``) + return + } + // The vendored group plugin: its config is a nested entry list. + if (name === '@cordisjs/plugin-group') { + if (Array.isArray(entry['config'])) checkEntryList(entry['config'], known, block, violations) + return + } + if (!known.has(name)) { + flag(`${at}: unknown plugin \`${name}\` (not a workspace package)`) + return + } + if (!name.startsWith('@deepseek-ai/dsh-')) return + const catalog = catalogFor(name) + if (!catalog) return + const config = asMapping(entry['config']) + if (catalog.kind === 'config') { + if (!config) return + const allowed = allowedConfigKeys(catalog) + for (const key of Object.keys(config)) { + if (!allowed.has(key)) { + flag(`${at}: \`${name}\` has no config key \`${key}\` (known keys: ${[...allowed].sort().join(', ')})`) + } + } + } else if (catalog.kind === 'no-config') { + if (config && Object.keys(config).length > 0) { + flag(`${at}: \`${name}\` declares no config, but the example passes one`) + } + } + // seam / library: loading one directly is dubious, but that is a prose + // concern — this gate only vouches for name existence. + }) +} + +const files = globSync('website/zh-CN/**/*.md', { cwd: root }) + .filter(f => !f.startsWith('website/zh-CN/api/')) + .sort() + +const violations: Violation[] = [] +const known = knownPackages() +let entryLists = 0 +let fragments = 0 +let ignored = 0 +let scanned = 0 + +for (const file of files) { + for (const block of extractBlocks(file)) { + scanned++ + if (block.kind === 'ignore') { + ignored++ + continue + } + let parsed: unknown + try { + parsed = yaml.load(block.code, { schema }) + } catch (error) { + const message = error instanceof Error ? error.message.split('\n')[0] ?? 'parse error' : String(error) + violations.push({ file: block.file, line: block.line, message: `yaml parse error: ${message}` }) + continue + } + if (Array.isArray(parsed)) { + entryLists++ + checkEntryList(parsed, known, block, violations) + } else { + // Mapping or scalar root: a fragment (e.g. a bare `config:` excerpt) — + // syntax is all there is to check. + fragments++ + } + } +} + +if (violations.length === 0) { + console.log( + `verify-website-yaml: ${scanned} yaml block(s) in ${files.length} file(s): ` + + `${entryLists} entry list(s) + ${fragments} fragment(s) checked, ${ignored} ignore-check skipped.`, + ) + process.exit(0) +} + +console.error('verify-website-yaml: invalid yaml examples found:') +for (const v of violations) { + console.error(` ${v.file}:${v.line} ${v.message}`) +} +process.exit(1) diff --git a/vendor/README.md b/vendor/README.md index 9bf226fb4d..ae43760ceb 100644 --- a/vendor/README.md +++ b/vendor/README.md @@ -36,6 +36,7 @@ Keep this log exhaustive — every divergence from upstream must be listed. 4. **Vendored TypeScript source internal specifiers**: changed local relative imports/exports from upstream's specifier shape to explicit `.ts` specifiers so TypeScript rewrites emitted JS to `.js` while declarations keep explicit, NodeNext-safe `.ts` specifiers. This includes `loader/src/config/isolate.ts` using `declare module './entry.ts'`. 5. **`schemastery/tsdown.config.ts` and `logger-console/tsdown.config.ts`**: ours, not upstream files — per-package build-shape overrides (dual ESM+CJS output; separate node/browser entries) for the repo-root tsdown build. They read the JS emitted under `lib/types` and then write the publish runtime entries under `lib/`. Like the regenerated tsconfigs, they are not part of the upstream sync surface. 6. **`cordis/src/fiber.ts` lifecycle hardening**: locally closes three reentrant disposal gaps. An effect's owner-list wrapper is registered before its setup body runs, so an unload begun from inside setup awaits setup and every collected cleanup; synchronous setup failure removes the wrapper and rolls back collected cleanup. Async cleanup stays owner-visible until quiescence, and Cordis's internal effect composition joins an already-running cleanup while repeated public disposer calls retain their upstream single-shot result. Effect creation is rejected while the owner is `UNLOADING` (while `PENDING` and `LOADING` remain legal), preventing cleanup-time registrations from escaping the unload snapshot. Child fibers register and receive their parent-owned disposer before `internal/plugin` publication, resolve dependency declarations added by that notification before activation, drain effects attached while pending, skip plugin execution when reentrant disposal invalidates the load epoch before its first checkpoint, and contain teardown-notification failures per observer so one callback cannot starve peers or interrupt ownership cleanup. +7. **`cordis/src/*.ts` JSDoc enrichment**: added `@param`/`@returns` tags and contract documentation (disposal semantics, waterfall veto, bail conditions, error cases) across the public plugin-author surface — `Context` (class, statics, and the `Context` interface properties incl. `root`), `EventsService`, `Fiber`, `RegistryService`, `ReflectService`, `Service`, `LoggerService` and their `declare module './context.ts'` overloads. Comment-only; no code changes. Motivation: the website API-reference generator renders these docs and hard-errors on undocumented members. Retire this entry when the enrichment is upstreamed to the fork. ## Sync procedure diff --git a/vendor/cordis/src/context.ts b/vendor/cordis/src/context.ts index 8b21c464b2..514b1e062a 100644 --- a/vendor/cordis/src/context.ts +++ b/vendor/cordis/src/context.ts @@ -14,14 +14,21 @@ import { Fiber } from './fiber.ts' * be read from `ctx`. */ export interface Context { + /** Isolation map: service name → scope label. Lookups for a name resolve within its label. */ [symbols.isolate]: Dict + /** Intercept map: service name → config merged into that service's per-plugin config. */ [symbols.intercept]: Dict - /** @experimental */ + /** The root context of the application (every child context shares it). @experimental */ root: this + /** Base URL used to resolve relative plugin/module specifiers, if the runtime sets one. */ baseUrl?: string + /** The event bus. Its methods are also mixed onto `ctx` (`ctx.on`, `ctx.emit`, ...). */ events: EventsService + /** The logging service. Call `ctx.logger(name)` for a named logger. */ logger: LoggerService + /** The reflection layer backing the context proxy (`ctx.get`, `ctx.provide`, ...). */ reflect: ReflectService + /** The plugin registry. Its methods are mixed onto `ctx` (`ctx.plugin`, `ctx.inject`). */ registry: RegistryService } @@ -33,12 +40,24 @@ export interface Context { * contexts without mutating their parent. */ export class Context { + /** Symbol key under which a disposer exposes its {@link EffectMeta} diagnostics tree. */ static readonly effect: unique symbol = symbols.effect + /** Symbol key for a context's listener filter, consulted on every event dispatch. */ static readonly filter: unique symbol = symbols.filter + /** Symbol key of the isolation map (see the `Context[symbols.isolate]` property). */ static readonly isolate: unique symbol = symbols.isolate + /** Symbol key of the intercept map (see the `Context[symbols.intercept]` property). */ static readonly intercept: unique symbol = symbols.intercept - /** Returns true for Cordis context proxies and context prototypes. */ + /** + * Returns true for Cordis context proxies and context prototypes. + * + * Works across realms and across multiple copies of cordis, because the + * brand is keyed by a global symbol rather than by `instanceof`. + * + * @param value — the value to test. + * @returns `true` if `value` is a Cordis context, narrowing its type. + */ static is(value: any): value is Context { return !!value?.[Context.is as any] } @@ -68,7 +87,15 @@ export class Context { return `Context <${this.fiber.name}>` } - /** Create a child context with extra metadata on top of the current scope. */ + /** + * Create a child context with extra metadata on top of the current scope. + * + * The child prototypally inherits every property of this context; own + * properties of `meta` shadow the inherited ones. The parent is not mutated. + * + * @param meta — own properties (including symbol keys) to define on the child. + * @returns a child context inheriting from this one. + */ extend(meta = {}): this { const shadow = Reflect.getOwnPropertyDescriptor(this, symbols.shadow)?.value const self = Object.create(getTraceable(this, this)) @@ -79,14 +106,36 @@ export class Context { return Object.assign(Object.create(self), { [symbols.shadow]: shadow }) } - /** Create a child context with an independent service scope for `name`. */ + /** + * Create a child context with an independent service scope for `name`. + * + * Below the returned context, reads and writes of the service `name` + * resolve against the new label instead of the parent's, so a different + * implementation can be provided without affecting the parent scope. + * Passing the same `label` to two `isolate()` calls joins their scopes. + * + * @param name — the service name to isolate. + * @param label — scope label to join; defaults to a fresh unique symbol. + * @returns a child context whose `name` service resolves in the new scope. + */ isolate(name: string, label?: symbol) { const shadow = Object.create(this[symbols.isolate]) shadow[name] = label ?? Symbol(name) return this.extend({ [symbols.isolate]: shadow }) } - /** Add service-specific intercept config for plugins started below this context. */ + /** + * Add service-specific intercept config for plugins started below this + * context. + * + * Plugins loaded under the returned context see `config` merged into the + * service's resolved config (ancestor entries first; see + * `Service[symbols.resolveConfig]`). The parent context is not affected. + * + * @param name — the service name whose config to intercept. + * @param config — the intercept config to merge for that service. + * @returns a child context carrying the additional intercept entry. + */ intercept(name: K, config: Context[K] extends { [symbols.config]: infer T } ? T : never): this intercept(name: string, config: any): this intercept(name: string, config: any) { diff --git a/vendor/cordis/src/events.ts b/vendor/cordis/src/events.ts index 842a780cdb..09589d69da 100644 --- a/vendor/cordis/src/events.ts +++ b/vendor/cordis/src/events.ts @@ -3,7 +3,12 @@ import { Context } from './context.ts' import { Fiber, FiberState } from './fiber.ts' import { DisposableList, symbols } from './utils.ts' -/** Return whether an event result should stop a bail-style dispatch. */ +/** + * Return whether an event result should stop a bail-style dispatch. + * + * @param value — a listener's return value. + * @returns `true` unless `value` is `null`, `false`, or `undefined`. + */ export function isBailed(value: any) { return value !== null && value !== false && value !== undefined } @@ -28,17 +33,75 @@ export type DispatchMode = 'emit' | 'parallel' | 'serial' | 'bail' | 'waterfall' declare module './context.ts' { export interface Context { /* eslint-disable max-len */ + /** + * Dispatch an event, running all listeners concurrently. + * + * @param name — the event name. + * @param args — arguments passed to every listener. + * @returns a promise resolving once every listener has settled. + */ parallel(name: K, ...args: Parameters): Promise + /** Same as above, with an explicit `this` for listeners (also used for filtering). */ parallel(thisArg: NoInfer>, name: K, ...args: Parameters): Promise + /** + * Dispatch an event synchronously, ignoring listener return values. + * + * @param name — the event name. + * @param args — arguments passed to every listener. + */ emit(name: K, ...args: Parameters): void + /** Same as above, with an explicit `this` for listeners (also used for filtering). */ emit(thisArg: NoInfer>, name: K, ...args: Parameters): void + /** + * Dispatch an event, awaiting listeners in order until one bails. + * + * @param name — the event name. + * @param args — arguments passed to each listener. + * @returns the first bail value (non-null, non-false, non-undefined), if any. + */ serial(name: K, ...args: Parameters): Promisify> + /** Same as above, with an explicit `this` for listeners (also used for filtering). */ serial(thisArg: NoInfer>, name: K, ...args: Parameters): Promisify> + /** + * Dispatch an event, calling listeners in order until one bails. + * + * @param name — the event name. + * @param args — arguments passed to each listener. + * @returns the first bail value (non-null, non-false, non-undefined), if any. + */ bail(name: K, ...args: Parameters): ReturnType + /** Same as above, with an explicit `this` for listeners (also used for filtering). */ bail(thisArg: NoInfer>, name: K, ...args: Parameters): ReturnType + /** + * Dispatch an event whose last argument is a `next` continuation. + * + * Each listener wraps the rest of the chain: calling `next()` invokes the + * next listener (finally the built-in behavior); not calling it vetoes. + * + * @param name — the event name. + * @param args — listener arguments; the final one is the innermost `next`. + * @returns the outermost listener's return value. + */ waterfall(name: K, ...args: Parameters): ReturnType + /** Same as above, with an explicit `this` for listeners (also used for filtering). */ waterfall(thisArg: NoInfer>, name: K, ...args: Parameters): ReturnType + /** + * Register an event listener owned by the current fiber. + * + * @param name — the event name to listen for. + * @param listener — called with the dispatch arguments. + * @param options — listener options; a boolean is shorthand for `prepend`. + * @returns a disposer removing the listener; `true` if it was still registered. + */ on(name: K, listener: Events[K], options?: boolean | EventOptions): () => boolean + /** + * Same as `on()`, but the listener disposes itself after its first call. + * + * @param name — the event name to listen for. + * @param listener — called at most once with the dispatch arguments. + * @param options — listener options; a boolean is shorthand for `prepend`. + * @returns a disposer removing the listener; `true` if it was still registered. + */ once(name: K, listener: Events[K], options?: boolean | EventOptions): () => boolean /* eslint-enable max-len */ } @@ -91,7 +154,13 @@ export class EventsService { }, { global: true, prepend: true }) } - /** Resolve listeners for one dispatch and apply context filtering. */ + /** + * Resolve listeners for one dispatch and apply context filtering. + * + * @param type — the dispatch mode, reported on `internal/dispatch`. + * @param args — the raw dispatch arguments; consumed up to the event name. + * @returns the matching listener callbacks, bound to the dispatch `this`. + */ dispatch(type: string, args: any[]) { const thisArg = typeof args[0] === 'object' || typeof args[0] === 'function' ? args.shift() : null const name: string = args.shift() @@ -104,19 +173,33 @@ export class EventsService { .map(hook => hook.callback.bind(thisArg)) } - /** Run listeners concurrently and wait for all of them. */ + /** + * Run listeners concurrently and wait for all of them. + * + * @param args — optional `this`, the event name, then listener arguments. + * @returns a promise resolving once every listener has settled. + */ async parallel(...args: any[]) { const results = await Promise.allSettled(this.dispatch('emit', args).map(async cb => cb(...args))) const errors = results.filter((result): result is PromiseRejectedResult => result.status === 'rejected') if (errors.length) throw new AggregateError(errors.map(error => error.reason)) } - /** Run listeners synchronously without waiting for returned promises. */ + /** + * Run listeners synchronously without waiting for returned promises. + * + * @param args — optional `this`, the event name, then listener arguments. + */ emit(...args: any[]) { this.dispatch('emit', args).map(cb => cb(...args)) } - /** Run listeners in order until one returns a bail value. */ + /** + * Run listeners in order, awaiting each, until one returns a bail value. + * + * @param args — optional `this`, the event name, then listener arguments. + * @returns the first bail value (see {@link isBailed}), if any. + */ async serial(...args: any[]) { for (const cb of this.dispatch('serial', args)) { const result = await cb(...args) @@ -124,7 +207,12 @@ export class EventsService { } } - /** Run listeners synchronously until one returns a bail value. */ + /** + * Run listeners synchronously until one returns a bail value. + * + * @param args — optional `this`, the event name, then listener arguments. + * @returns the first bail value (see {@link isBailed}), if any. + */ bail(...args: any[]) { for (const cb of this.dispatch('bail', args)) { const result = cb(...args) @@ -132,7 +220,16 @@ export class EventsService { } } - /** Compose listeners around the final `next` callback. */ + /** + * Compose listeners around the final `next` callback. + * + * The last dispatch argument is treated as the innermost `next`. Listeners + * run outermost-first; a listener that does not call `next()` vetoes the + * rest of the chain, including the built-in behavior. + * + * @param args — optional `this`, the event name, listener arguments, then `next`. + * @returns the outermost listener's return value. + */ waterfall(...args: any[]) { const cbs = this.dispatch('waterfall', args) const inner = args.pop() @@ -144,6 +241,15 @@ export class EventsService { return next() } + /** + * Store a listener record as an effect on the current fiber. + * + * @param label — effect label shown in fiber diagnostics. + * @param hooks — the listener list for one event. + * @param callback — the listener to store. + * @param options — placement and filtering options. + * @returns a disposer that unregisters the listener. + */ register(label: string, hooks: Hook[], callback: any, options: EventOptions): () => void { const method = options.prepend ? 'unshift' : 'push' return this.ctx.fiber.effect(() => { @@ -152,6 +258,13 @@ export class EventsService { }, label) } + /** + * Remove a stored listener record. + * + * @param hooks — the listener list for one event. + * @param callback — the listener to remove. + * @returns `true` if the listener was found and removed. + */ unregister(hooks: Hook[], callback: any) { const index = hooks.findIndex(hook => hook.callback === callback) if (index >= 0) { @@ -160,7 +273,17 @@ export class EventsService { } } - /** Register an event listener owned by the current fiber. */ + /** + * Register an event listener owned by the current fiber. + * + * The listener is removed automatically when the fiber unloads. Throws + * `CordisError('INACTIVE_EFFECT')` if the fiber is already disposed. + * + * @param name — the event name to listen for. + * @param listener — called with the dispatch arguments. + * @param options — listener options; a boolean is shorthand for `prepend`. + * @returns a disposer removing the listener; `true` if it was still registered. + */ on(name: string | symbol, listener: (...args: any) => any, options?: boolean | EventOptions) { if (typeof options !== 'object') { options = { prepend: options } @@ -177,7 +300,14 @@ export class EventsService { return this.register(label, hooks, listener, options) } - /** Register an event listener that disposes itself after the first call. */ + /** + * Register an event listener that disposes itself after the first call. + * + * @param name — the event name to listen for. + * @param listener — called at most once with the dispatch arguments. + * @param options — listener options; a boolean is shorthand for `prepend`. + * @returns a disposer removing the listener; `true` if it was still registered. + */ once(name: string, listener: (...args: any) => any, options?: boolean | EventOptions) { const dispose = this.on(name, function (...args: any[]) { dispose() @@ -196,12 +326,20 @@ export class EventsService { * diagnostics before public events are delivered. */ export interface Events { + /** A plugin fiber was created or its uid was cleared on disposal. */ 'internal/plugin'(fiber: Fiber): void + /** A fiber changed lifecycle state; receives the fiber and its previous state. */ 'internal/status'(fiber: Fiber, oldValue: FiberState): void + /** Interception hook for a service binding (no core producer). */ 'internal/service'(this: Context, name: string, value: any): void + /** Waterfall: a fiber config update is being applied; skip `next()` to veto. */ 'internal/update'(this: Fiber, config: any, noSave: boolean, next: () => void): void + /** Waterfall: a service is being read through the context proxy. */ 'internal/get'(ctx: Context, name: string, error: Error, next: () => any): any + /** Waterfall: a service is being written through the context proxy. */ 'internal/set'(ctx: Context, name: string, value: any, error: Error, next: () => boolean): boolean + /** Bail: a listener is being registered; a non-null result replaces registration. */ 'internal/listener'(this: Context, name: string, listener: any, prepend: boolean): void + /** An event is being dispatched to listeners (fired for non-internal events only). */ 'internal/dispatch'(mode: DispatchMode, name: string, args: any[], thisArg: any): void } diff --git a/vendor/cordis/src/fiber.ts b/vendor/cordis/src/fiber.ts index 43a320142f..6beb7bbc24 100644 --- a/vendor/cordis/src/fiber.ts +++ b/vendor/cordis/src/fiber.ts @@ -7,6 +7,7 @@ import { StandardSchemaV1 } from '@standard-schema/spec' declare module './context.ts' { export interface Context extends Pick { + /** The fiber (plugin runtime instance) that owns this context. */ fiber: Fiber } } @@ -17,6 +18,11 @@ const kValidationError = Symbol.for('ValidationError') export class ValidationError extends TypeError { name = 'ValidationError' + /** + * Build the aggregated message from schema issues. + * + * @param issues — the standard-schema issues, one message line each. + */ constructor(issues: readonly StandardSchemaV1.Issue[]) { super(`invalid config:\n` + issues.map(issue => { if (issue.path) { @@ -32,7 +38,14 @@ Object.defineProperty(ValidationError.prototype, kValidationError, { value: true, }) -/** Validate and normalize config for a plugin runtime before it starts. */ +/** + * Validate and normalize config for a plugin runtime before it starts. + * + * @param runtime — the plugin runtime whose `Config` schema to apply. + * @param config — the raw user config. + * @returns the validated config, or `config` unchanged if the runtime has no schema. + * @throws {ValidationError} when validation reports issues. + */ export function resolveConfig(runtime: Plugin.Runtime, config: any) { if (!runtime.Config) return config // TODO: async validation @@ -51,10 +64,21 @@ interface AsyncDisposable = Awaitable> extends P (): T } -/** Function returned by an effect to release resources during disposal. */ +/** + * Function returned by an effect to release resources during disposal. + * + * Disposers run in reverse registration order when the owning fiber unloads; + * they may be async, in which case unloading awaits them. + */ export type Disposable = () => T -/** Effect body result accepted by `ctx.effect()` and plugin startup. */ +/** + * Effect body result accepted by `ctx.effect()` and plugin startup. + * + * Either a single disposer, a promise of one, or a (possibly async) iterable + * yielding several — generator effects register each yielded disposer as it + * is produced. + */ export type Effect = | SyncEffect | AsyncEffect @@ -69,7 +93,9 @@ type AsyncEffect = /** Tree node used to expose nested effect labels for diagnostics. */ export interface EffectMeta { + /** Human-readable effect label, e.g. `ctx.on("event")` or `ctx.provide("name")`. */ label: string + /** Metadata of nested effects registered while this effect ran. */ children: EffectMeta[] } @@ -109,7 +135,14 @@ function emitPluginDisposed(context: Context, fiber: Fiber) { } } -/** Lifecycle state for one plugin fiber. */ +/** + * Lifecycle state for one plugin fiber. + * + * `PENDING` — waiting for required services; `LOADING` — the plugin callback + * is running; `ACTIVE` — loaded and providing; `FAILED` — the callback or its + * config threw; `UNLOADING` — disposers are running; `DISPOSED` — the fiber + * was removed and cannot restart. + */ export const enum FiberState { PENDING, LOADING, @@ -121,6 +154,10 @@ export const enum FiberState { /** Framework error with a stable machine-readable code. */ export class CordisError extends Error { + /** + * @param code — the stable error code; also the default message. + * @param message — optional human-readable override. + */ constructor(public code: CordisError.Code, message?: string) { super(message ?? CordisError.Code[code]) } @@ -144,12 +181,19 @@ const INACTIVE = '__INACTIVE__' * cleanup for the plugin context returned by `ctx.plugin()`. */ export class Fiber { + /** Unique id within the registry; 0 for the root fiber, `null` once disposed. */ public uid: number | null + /** The context this fiber's plugin runs in (extends the parent context). */ public readonly ctx: Context + /** The validated plugin config (updated by `update()`). */ public config: any + /** Current lifecycle state; transitions emit `internal/status`. */ public state = FiberState.PENDING + /** Dispose this fiber: unload the plugin, then settle once cleanup finished. */ public readonly dispose: () => Promise + /** Snapshot of required service implementations while loaded; `undefined` otherwise. */ public store: Dict | undefined + /** The in-flight load/unload transition, if one is currently running. */ public inertia: Promise | undefined public readonly _hooks: Dict> = Object.create(null) @@ -162,6 +206,16 @@ export class Fiber { private _runner: EffectRunner private _store: Dict = Object.create(null) + /** + * Create a fiber. Plugin authors normally obtain fibers from `ctx.plugin()` + * rather than constructing them directly. + * + * @param parent — the context the plugin was loaded from. + * @param config — raw config, validated against the runtime's schema. + * @param inject — resolved dependency map (service name → intercept config). + * @param runtime — the shared plugin runtime, or `null` for the root fiber. + * @param getOuterStack — captures the caller stack for effect diagnostics. + */ constructor( public parent: Context, config: any, @@ -282,6 +336,7 @@ export class Fiber { } } + /** The plugin's display name, inherited from the nearest named ancestor, else `'root'`. */ get name() { let fiber: Fiber = this do { @@ -291,7 +346,12 @@ export class Fiber { return 'root' } - /** Throw if the fiber has already been disposed. */ + /** + * Throw if the fiber has already been disposed. + * + * @returns nothing when the fiber is still active. + * @throws {CordisError} `INACTIVE_EFFECT` when the fiber's uid has been cleared. + */ assertActive() { if (this.uid !== null) return throw new CordisError('INACTIVE_EFFECT') @@ -343,8 +403,21 @@ export class Fiber { }, runner.getOuterStack) } - /** Register a cleanup-aware effect on this fiber. */ + /** + * Register a cleanup-aware effect on this fiber. + * + * `execute` runs immediately; the disposers it produces are collected and + * run (in reverse order) either when the returned disposer is called or + * when the fiber unloads, whichever comes first. Calling the disposer twice + * is a no-op. Throws `CordisError('INACTIVE_EFFECT')` if the fiber is + * already disposed, and `TypeError` if `execute` returns an invalid shape. + * + * @param execute — the effect body; see {@link Effect} for accepted shapes. + * @param label — effect label shown in `getEffects()` diagnostics. + * @returns a disposer that tears the effect down and settles once done. + */ effect(execute: () => SyncEffect, label?: string): Disposable> + /** Same as above for async effects; the disposer is also awaitable. */ effect(execute: () => Effect, label?: string): AsyncDisposable> effect(execute: () => Effect, label = 'anonymous'): any { this.assertActive() @@ -491,7 +564,11 @@ export class Fiber { return wrapper } - /** Return metadata for currently registered effects. */ + /** + * Return metadata for currently registered effects. + * + * @returns one {@link EffectMeta} tree per labeled live effect. + */ getEffects() { return [...this._disposables] .map(dispose => dispose[symbols.effect]) @@ -615,7 +692,12 @@ export class Fiber { }) } - /** Wait for current lifecycle work and rethrow startup errors. */ + /** + * Wait for current lifecycle work and rethrow startup errors. + * + * @returns this fiber, once it has settled into a stable state. + * @throws the config-validation or plugin-startup error, if any. + */ async await() { while (this.inertia) { await this.inertia @@ -624,7 +706,12 @@ export class Fiber { return this } - /** Dispose and immediately reload this plugin with its current config. */ + /** + * Dispose and immediately reload this plugin with its current config. + * + * @returns a promise resolving once the reload settled. + * @throws {CordisError} `INACTIVE_EFFECT` when the fiber is already disposed. + */ async restart() { this.assertActive() this._setEpoch(INACTIVE) @@ -632,7 +719,17 @@ export class Fiber { await this.await() } - /** Validate and apply new config, then restart the plugin. */ + /** + * Validate and apply new config, then restart the plugin. + * + * Runs the `internal/update` waterfall first, so update hooks (and HMR) + * can veto or replace the restart. + * + * @param config — the new raw config; validated before anything restarts. + * @param noSave — hint for persistence hooks not to write the change back. + * @returns nothing; the restart runs behind the `internal/update` waterfall. + * @throws {ValidationError} when the new config fails validation. + */ update(config: any, noSave = false) { this.assertActive() config = resolveConfig(this.runtime!, config) diff --git a/vendor/cordis/src/logger.ts b/vendor/cordis/src/logger.ts index a1e97c165a..3c5ad10525 100644 --- a/vendor/cordis/src/logger.ts +++ b/vendor/cordis/src/logger.ts @@ -62,8 +62,11 @@ export const defaultFormatters: Record = { /** Options used when creating a named logger facade. */ export interface LoggerOptions { + /** The logger name shown with each message. */ name: string + /** Message fields merged into every record from this logger. */ meta?: Partial + /** Default maximum level exported when an exporter has no own threshold. */ level?: number } @@ -220,7 +223,12 @@ export class LoggerService { return self } - /** Register an exporter and dispose it with the current fiber. */ + /** + * Register an exporter and dispose it with the current fiber. + * + * @param exporter — the sink that receives structured log messages. + * @returns a disposer that removes the exporter. + */ exporter(exporter: Exporter) { return this.ctx.effect(() => { this.exporters.set(++this._snExporter, exporter) diff --git a/vendor/cordis/src/reflect.ts b/vendor/cordis/src/reflect.ts index c084067af1..e91cb63fea 100644 --- a/vendor/cordis/src/reflect.ts +++ b/vendor/cordis/src/reflect.ts @@ -5,14 +5,66 @@ import { Fiber, FiberState } from './fiber.ts' declare module './context.ts' { interface Context { + /** + * Read a service from the store without the inject requirement. + * + * @param name — the service name. + * @param strict — when `true` (default), only return implementations + * whose providing fiber is currently active. + * @returns the service value, or `undefined` when not (yet) provided. + */ get(name: K, strict?: boolean): undefined | this[K] + /** Same as above for service names outside the typed `Context` surface. */ get(name: string, strict?: boolean): any + /** + * Overwrite a provided service's value. + * + * Only the fiber that provided the service may set it; setting an + * unprovided name throws. + * + * @param name — the service name. + * @param value — the new service value. + */ set(name: K, value: undefined | this[K]): void + /** Same as above for service names outside the typed `Context` surface. */ set(name: string, value: any): void + /** + * Register a service implementation owned by the current fiber. + * + * The service becomes visible to dependents in the same isolation scope + * once the fiber is active; it is unregistered (waking dependents) when + * the returned disposer runs or the fiber unloads. Throws if the name is + * already provided in this scope or declared as an accessor. + * + * @param name — the service name. + * @param value — the service value. + * @returns a disposer that unregisters the service. + */ provide(name: K, value: undefined | this[K]): () => void + /** Same as above for service names outside the typed `Context` surface. */ provide(name: string, value?: any): () => void + /** + * Define a computed context property backed by get/set hooks. + * + * The accessor is removed when the current fiber unloads. Throws if the + * name is already declared. + * + * @param name — the context property name. + * @param options — the `get` hook and optional `set` hook. + */ accessor(name: string, options: Omit): void + /** + * Expose selected members of a service directly on `ctx`. + * + * Each mixed-in key becomes an accessor that forwards to the service + * (binding methods to it), so e.g. `ctx.on` forwards to `ctx.events.on`. + * Mixins are removed when the current fiber unloads. + * + * @param name — the context property holding the source service. + * @param mixins — keys to forward, or a source-key → ctx-key map. + */ mixin(name: K, mixins: (keyof this & keyof this[K])[] | Dict): void + /** Same as above with a source object instead of a context property name. */ mixin(source: T, mixins: (keyof this & keyof T)[] | Dict): void } } @@ -44,22 +96,30 @@ export type Property = Property.Service | Property.Accessor export namespace Property { /** Service property backed by a provided implementation. */ export interface Service { + /** Discriminator. */ type: 'service' } /** Computed context property backed by custom get/set hooks. */ export interface Accessor { + /** Discriminator. */ type: 'accessor' + /** Compute the property value; `error` carries the caller stack for diagnostics. */ get: (this: Context, receiver: any, error: Error) => any + /** Optional setter; return `false` to reject the write. */ set?: (this: Context, value: any, receiver: any, error: Error) => boolean } } /** Concrete service implementation record stored in the root reflect service. */ export interface Impl { + /** The service name. */ name: string + /** The fiber that provided the service (owns its lifetime). */ fiber: Fiber + /** The current service value. */ value?: any + /** Optional availability predicate consulted before dependents may load. */ check?: () => boolean } @@ -70,6 +130,7 @@ export interface Impl { * the mixins that expose core service methods directly on `ctx`. */ export class ReflectService { + /** Proxy traps implementing service resolution for every context object. */ static handler: ProxyHandler = { get: (target, prop, ctx: Context) => { if (isSpecialProperty(prop)) { @@ -143,7 +204,9 @@ export class ReflectService { }, } + /** Service implementations, keyed by isolation label. */ public store: Dict = Object.create(null) + /** Declared context properties (services and accessors), by name. */ public props: Dict = Object.create(null) constructor(public ctx: Context) { @@ -158,6 +221,14 @@ export class ReflectService { this.mixin('events', ['on', 'once', 'parallel', 'emit', 'serial', 'bail', 'waterfall']) } + /** + * Read a service from the store without the inject requirement. + * + * @param name — the service name. + * @param strict — when `true`, only return implementations whose providing + * fiber is currently active. + * @returns the service value, or `undefined` when not (yet) provided. + */ get(name: string, strict = true) { return getTraceable(this.ctx, this._getImpl(name, strict)?.value) } @@ -170,6 +241,15 @@ export class ReflectService { return impl } + /** + * Overwrite a provided service's value. + * + * @param name — the service name. + * @param value — the new service value. + * @param error — carrier for the caller stack in diagnostics. + * @returns `true` on success. + * @throws when `name` was never provided, or was provided by another fiber. + */ set(name: string, value: any, error?: Error) { const key = this.ctx[symbols.isolate][name] const impl = this.store[key] @@ -183,6 +263,16 @@ export class ReflectService { return true } + /** + * Register a service implementation owned by the current fiber. + * + * See the `ctx.provide()` overload above for the full contract. + * + * @param name — the service name. + * @param value — the service value. + * @param check — optional availability predicate for dependents. + * @returns a disposer that unregisters the service. + */ provide(name: string, value?: any, check?: () => boolean) { return this.ctx.fiber.effect(() => { if (!this.props[name]) { @@ -213,6 +303,13 @@ export class ReflectService { }, `ctx.provide(${JSON.stringify(name)})`) } + /** + * Re-evaluate every fiber that requires one of the given services. + * + * @param names — the service names that changed. + * @param filter — restricts notification to matching isolation scopes. + * @returns the fibers whose dependency state was refreshed. + */ notify(names: string[], filter = (ctx: Context, name: string) => ctx[symbols.isolate][name] === this.ctx[symbols.isolate][name]) { const fibers: Fiber[] = [] for (const runtime of this.ctx.registry.values()) { @@ -237,6 +334,13 @@ export class ReflectService { return fibers } + /** + * Define a computed context property backed by get/set hooks. + * + * @param name — the context property name. + * @param options — the `get` hook and optional `set` hook. + * @returns a disposer that removes the accessor. + */ accessor(name: string, options: Omit) { return this.ctx.fiber.effect(() => { if (name in this.props) { @@ -247,6 +351,15 @@ export class ReflectService { }, `ctx.accessor(${JSON.stringify(name)})`) } + /** + * Expose selected members of a service directly on `ctx`. + * + * See the `ctx.mixin()` overload above for the full contract. + * + * @param source — a context property name or a source object. + * @param mixins — keys to forward, or a source-key → ctx-key map. + * @returns a disposer that removes all created accessors. + */ mixin(source: any, mixins: string[] | Dict) { const self = this return this.ctx.fiber.effect(function* () { @@ -275,10 +388,22 @@ export class ReflectService { }, `ctx.mixin(${JSON.stringify(source)})`) } + /** + * Attach this context's tracing wrapper to a value. + * + * @param value — the value to wrap. + * @returns the traceable wrapper (or the value itself when not applicable). + */ trace(value: T) { return getTraceable(this.ctx, value) } + /** + * Wrap a callback so calls trace `this` and arguments to this context. + * + * @param callback — the function to wrap. + * @returns a proxy delegating to `callback` with traced values. + */ bind(callback: T) { return new Proxy(callback, { apply: (target, thisArg, args) => { diff --git a/vendor/cordis/src/registry.ts b/vendor/cordis/src/registry.ts index 9dfa10a06b..05fbadcfad 100644 --- a/vendor/cordis/src/registry.ts +++ b/vendor/cordis/src/registry.ts @@ -28,6 +28,11 @@ export type InjectKey = keyof { * On classes it contributes to the plugin's static `inject` map. On methods it * delays the method call until the declared services are available. */ +/** + * @param name — the required service name. + * @param config — optional intercept config applied for that service. + * @returns the class or method decorator. + */ export function Inject(name: K, config?: Context[K] extends { [symbols.config]: infer T } ? T : never) { return function (value: any, decorator: ClassDecoratorContext | ClassMethodDecoratorContext) { if (decorator.kind === 'class') { @@ -55,7 +60,13 @@ export function Inject(name: K, config?: Context[K] extends /** Utilities for normalizing plugin dependency declarations. */ export namespace Inject { - /** Convert array/object/class-inherited inject metadata into a plain map. */ + /** + * Convert array/object/class-inherited inject metadata into a plain map. + * + * @param inject — the declaration to normalize; `null`/`undefined` add nothing. + * @param result — the map to fill (service name → intercept config or `null`). + * @returns `result`. + */ export function resolve(inject: Inject | null | undefined, result: Dict = Object.create(null)) { if (!inject) return result if (Array.isArray(inject)) { @@ -86,10 +97,15 @@ export type Plugin = export namespace Plugin { /** Shared metadata understood by the plugin registry and related tooling. */ export interface Base { + /** Display name used for fiber diagnostics and logger names. */ name?: string + /** Standard-schema validator applied to config before the plugin starts. */ Config?: StandardSchemaV1 + /** Services the plugin requires; it only loads while all are available. */ inject?: Inject + /** Service name(s) the plugin provides (read by `Service` and by loaders). */ provide?: string | string[] + /** Service names whose intercept config the plugin declares it consumes. */ intercept?: Dict } @@ -117,9 +133,13 @@ export namespace Plugin { /** Mutable registry record shared by all fibers of one plugin callback. */ export interface Runtime { + /** Display name copied from the first registered plugin shape. */ name?: string + /** Every live fiber of this plugin (one per `ctx.plugin()` call). */ fibers: DisposableList + /** The executable entrypoint all fibers share (registry identity key). */ callback: globalThis.Function + /** Standard-schema validator applied to each fiber's config. */ Config?: StandardSchemaV1 } } @@ -142,7 +162,25 @@ type GetPluginConfig

= declare module './context.ts' { export interface Context { + /** + * Run a callback once the requested services are available. + * + * Shorthand for `ctx.plugin({ inject, apply: callback })`: the callback + * is unloaded and re-run whenever a required service changes. + * + * @param deps — required services, as an array or a name → config map. + * @param callback — plugin body called with `(ctx, config)`. + * @returns the fiber; awaiting it settles once loading finished. + */ inject(deps: Inject, callback: Plugin.Function): Fiber & PromiseLike + /** + * Load a plugin in the current context. + * + * @param plugin — a function, class, or `{ apply }` object plugin. + * @param args — the plugin config, validated against its `Config` schema. + * @returns the fiber; awaiting it settles once loading finished + * (rejecting on config or startup errors). + */ plugin

(plugin: P, ...args: Spread>): Fiber & PromiseLike } } @@ -164,15 +202,22 @@ export class RegistryService { }) } + /** Allocate the next fiber uid (increments on every read). */ get counter() { return ++this._counter } + /** Number of registered plugin runtimes. */ get size() { return this._internal.size } - /** Resolve a supported plugin shape to its executable callback. */ + /** + * Resolve a supported plugin shape to its executable callback. + * + * @param plugin — a function, class, or `{ apply }` object plugin. + * @returns the callback identifying the plugin, or `undefined` if invalid. + */ resolve(plugin: Plugin): Function | undefined { // plugin.apply may throw try { @@ -181,17 +226,34 @@ export class RegistryService { } catch {} } + /** + * Look up the runtime record for a plugin. + * + * @param plugin — any supported plugin shape. + * @returns the runtime, or `undefined` when the plugin is not registered. + */ get(plugin: Plugin) { const key = this.resolve(plugin) return key && this._internal.get(key) } + /** + * Check whether a plugin has a registered runtime. + * + * @param plugin — any supported plugin shape. + * @returns `true` when at least one fiber of the plugin exists. + */ has(plugin: Plugin) { const key = this.resolve(plugin) return !!key && this._internal.has(key) } - /** Dispose every running fiber for a plugin and remove its runtime record. */ + /** + * Dispose every running fiber for a plugin and remove its runtime record. + * + * @param plugin — any supported plugin shape. + * @returns the removed runtime, or `undefined` when none was registered. + */ delete(plugin: Plugin) { const key = this.resolve(plugin) const runtime = key && this._internal.get(key) @@ -203,28 +265,53 @@ export class RegistryService { return runtime } + /** Iterate the registered plugin callbacks. */ keys() { return this._internal.keys() } + /** Iterate the registered plugin runtimes. */ values() { return this._internal.values() } + /** Iterate `[callback, runtime]` pairs. */ entries() { return this._internal.entries() } + /** + * Visit every registered runtime. + * + * @param callback — receives each runtime and its identifying callback. + */ forEach(callback: (value: Plugin.Runtime, key: Function) => void) { return this._internal.forEach(callback) } - /** Start a callback once the requested dependencies are available. */ + /** + * Start a callback once the requested dependencies are available. + * + * @param inject — required services, as an array or a name → config map. + * @param callback — plugin body called with `(ctx, config)`. + * @returns the fiber; awaiting it settles once loading finished. + */ inject(inject: Inject, callback: Plugin.Function) { return this.plugin({ inject, apply: callback, name: callback.name }) } - /** Start a plugin in the current context and return its fiber. */ + /** + * Start a plugin in the current context and return its fiber. + * + * Creates (or reuses) the plugin's runtime record, then starts a new fiber + * under the current context. Throws if `plugin` is not a supported shape or + * if the current fiber is already disposed. + * + * @param plugin — a function, class, or `{ apply }` object plugin. + * @param config — the plugin config, validated against its `Config` schema. + * @param getOuterStack — captures the caller stack for effect diagnostics. + * @returns the fiber; awaiting it settles once loading finished. + */ plugin(plugin: Plugin, config?: any, getOuterStack = buildOuterStack()) { // check if it's a valid plugin const callback = this.resolve(plugin) diff --git a/vendor/cordis/src/service.ts b/vendor/cordis/src/service.ts index 30895247c1..dc6622b68f 100644 --- a/vendor/cordis/src/service.ts +++ b/vendor/cordis/src/service.ts @@ -9,19 +9,36 @@ import { createCallable, joinPrototype, symbols, Tracker } from './utils.ts' * registered immediately and is automatically removed with the owning fiber. */ export abstract class Service { + /** Symbol key of an instance method run after construction (class plugins). */ static readonly init: unique symbol = symbols.init + /** Symbol key of the availability predicate passed to `ctx.provide()`. */ static readonly check: unique symbol = symbols.check + /** Symbol key of the phantom intercept-config type parameter. */ static readonly config: unique symbol = symbols.config + /** Symbol key of the call body making a service callable (e.g. `ctx.logger()`). */ static readonly invoke: unique symbol = symbols.invoke + /** Symbol key of the helper deriving an extended service instance. */ static readonly extend: unique symbol = symbols.extend + /** Symbol key of the tracker metadata used for context tracing. */ static readonly tracker: unique symbol = symbols.tracker + /** Symbol key of the intercept-config resolution helper below. */ static readonly resolveConfig: unique symbol = symbols.resolveConfig declare [symbols.config]: T + /** The service name this instance is registered under. */ public name!: string - /** Register this instance as `name` in the current context. */ + /** + * Register this instance as `name` in the current context. + * + * Calls `ctx.reflect.provide(name, this, this[Service.check])`, so the + * service is unregistered automatically when the owning fiber unloads. + * Services with a `[Service.invoke]` body return a callable instance. + * + * @param ctx — the context to register in (stored as `this.ctx`). + * @param name — the service name; defaults to the static `provide` field. + */ constructor(protected ctx: Context, name: string) { name ??= this.constructor['provide'] as string @@ -55,7 +72,17 @@ export abstract class Service { return Object.assign(self, props) } - /** Merge intercept config from ancestors with optional base and head values. */ + /** + * Merge intercept config from ancestors with optional base and head values. + * + * Entries added closer to the root apply first; `base` is prepended and + * `head` appended. Uses `Config.merge` when the service declares one, + * otherwise a shallow `Object.assign`. + * + * @param base — lowest-precedence config merged before all intercepts. + * @param head — highest-precedence config merged after all intercepts. + * @returns the merged config. + */ [symbols.resolveConfig](base?: T, head?: T): T { let intercept = this.ctx[Context.intercept] const configs: any[] = [] diff --git a/website/.gitignore b/website/.gitignore new file mode 100644 index 0000000000..2c1fa99cb4 --- /dev/null +++ b/website/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +.vitepress/dist/ +.vitepress/cache/ diff --git a/website/.vitepress/config/api-sidebar.json b/website/.vitepress/config/api-sidebar.json new file mode 100644 index 0000000000..cdb6e2bfac --- /dev/null +++ b/website/.vitepress/config/api-sidebar.json @@ -0,0 +1,122 @@ +{ + "cordis": [ + { + "text": "Context", + "link": "/zh-CN/api/cordis/context" + }, + { + "text": "Events", + "link": "/zh-CN/api/cordis/events" + }, + { + "text": "Fiber", + "link": "/zh-CN/api/cordis/fiber" + }, + { + "text": "Registry", + "link": "/zh-CN/api/cordis/registry" + }, + { + "text": "Service", + "link": "/zh-CN/api/cordis/service" + } + ], + "harness": [ + { + "text": "ctx.agentLoop", + "link": "/zh-CN/api/harness/agent-loop" + }, + { + "text": "ctx.agents", + "link": "/zh-CN/api/harness/agents" + }, + { + "text": "ctx.approval", + "link": "/zh-CN/api/harness/approval" + }, + { + "text": "ctx.bash", + "link": "/zh-CN/api/harness/bash" + }, + { + "text": "ctx.bashEnv", + "link": "/zh-CN/api/harness/bash-env" + }, + { + "text": "ctx.codeRuntime", + "link": "/zh-CN/api/harness/code-runtime" + }, + { + "text": "ctx.compact", + "link": "/zh-CN/api/harness/compact" + }, + { + "text": "ctx.fs", + "link": "/zh-CN/api/harness/fs" + }, + { + "text": "ctx.llm", + "link": "/zh-CN/api/harness/llm" + }, + { + "text": "ctx.permission", + "link": "/zh-CN/api/harness/permission" + }, + { + "text": "ctx.sandbox", + "link": "/zh-CN/api/harness/sandbox" + }, + { + "text": "ctx.sessionPersistence", + "link": "/zh-CN/api/harness/session-persistence" + }, + { + "text": "ctx.sessionQuery", + "link": "/zh-CN/api/harness/session-query" + }, + { + "text": "ctx.sessions", + "link": "/zh-CN/api/harness/sessions" + }, + { + "text": "ctx.skills", + "link": "/zh-CN/api/harness/skills" + }, + { + "text": "ctx.spillStore", + "link": "/zh-CN/api/harness/spill-store" + }, + { + "text": "ctx.subagents", + "link": "/zh-CN/api/harness/subagents" + }, + { + "text": "ctx.systemPrompt", + "link": "/zh-CN/api/harness/system-prompt" + }, + { + "text": "ctx.tasks", + "link": "/zh-CN/api/harness/tasks" + }, + { + "text": "ctx.tools", + "link": "/zh-CN/api/harness/tools" + }, + { + "text": "ctx.userInteraction", + "link": "/zh-CN/api/harness/user-interaction" + }, + { + "text": "ctx.web", + "link": "/zh-CN/api/harness/web" + }, + { + "text": "ctx.workflows", + "link": "/zh-CN/api/harness/workflows" + }, + { + "text": "Events", + "link": "/zh-CN/api/harness/events" + } + ] +} diff --git a/website/.vitepress/config/index.ts b/website/.vitepress/config/index.ts new file mode 100644 index 0000000000..764d8619a9 --- /dev/null +++ b/website/.vitepress/config/index.ts @@ -0,0 +1,24 @@ +import { defineConfig } from 'vitepress' +import { zhCN } from './zh-CN' + +export default defineConfig({ + title: 'DeepSeek Harness', + description: '插件化 Agent 开发框架', + + // The design essays (design/revertible-effects, design/context-model) carry + // real TeX; math: true wires markdown-it-mathjax3 into the pipeline. + // markdown-it-mathjax3 is pinned to ^4 (NOT 5.x): v5 injects a