From f317e8aa836b7e8f1ae22d0ba28f0c2604437712 Mon Sep 17 00:00:00 2001 From: X Date: Mon, 27 Jul 2026 11:52:21 -0400 Subject: [PATCH] Consolidate Reploid journeys and remove dead code --- README.md | 18 +- docs/API.md | 4 +- docs/INDEX.md | 8 + docs/TESTING.md | 6 +- .../reploid-descriptor-peer-orchestration.md | 4 +- docs/browser-inference-pool.md | 12 +- docs/critical-user-journeys.md | 27 + docs/multi-model-evaluation.md | 1 - docs/poolday/critical-user-journeys.md | 43 + .../poolday/participation-identity-routing.md | 11 +- .../poolday-critical-user-journeys.json | 452 +++++++ docs/status/surface-claim-index.json | 16 +- docs/status/x-critical-user-journeys.json | 460 +++++++ docs/status/zero-critical-user-journeys.json | 360 ++++++ docs/x/critical-user-journeys.md | 43 + docs/zero/critical-user-journeys.md | 44 + package.json | 4 + scripts/critical-user-journey-contract.js | 169 +++ scripts/validate-registry.js | 50 +- scripts/verify-critical-user-journeys.js | 25 + scripts/verify-pool-critical-user-journeys.js | 75 ++ scripts/verify-pool-production.js | 2 + scripts/verify-pool-release.js | 2 + scripts/verify-surface-claim-index.js | 66 +- scripts/verify-x-critical-user-journeys.js | 42 + scripts/verify-zero-critical-user-journeys.js | 42 + ...000068-hierarchical-memory-architecture.md | 2 +- .../blueprints/0x00012e-pool-policy-router.md | 6 +- self/blueprints/implementation-status.md | 212 ---- self/boot-helpers/config.js | 2 +- self/boot-helpers/vfs-hydrate.js | 4 +- self/boot-spec.js | 2 +- self/capabilities/README.md | 4 - self/capabilities/cognition/prompt-memory.js | 2 - self/config/blueprint-registry.json | 313 +---- self/config/boot-seed.js | 1 - self/config/genesis-levels.json | 20 +- self/config/genesis-template.json | 20 +- self/config/lab-route-profiles.js | 5 - self/config/vfs-manifest.json | 30 +- self/core/async-utils.js | 263 ---- self/host/seed-vfs.js | 3 +- self/lab/mirrors.js | 1 - self/manifest.js | 2 +- self/pool/TODO.md | 157 --- self/pool/layer-scheduler.js | 171 --- self/pool/peer-registry.js | 144 --- self/pool/policy-router.js | 81 +- self/pool/policy-validation.js | 68 + self/pool/pool-config.json | 14 +- self/pool/shard-negotiation.js | 188 --- self/styles/landing-mono.css | 284 ----- self/styles/proto/hitl.css | 283 ----- self/styles/proto/index.css | 1 - self/styles/proto/panels.css | 238 ---- self/styles/proto/responsive.css | 13 +- self/styles/vfs-explorer.css | 454 ------- self/testing/arena/doppler-integration.js | 535 -------- self/testing/arena/index.js | 10 - self/ui/UI.js | 7 - self/ui/boot-wizard/steps/detect.js | 146 --- self/ui/boot-wizard/zero-function.js | 1 - self/ui/capsule/index.js | 1 - self/ui/components/arena-results.js | 286 ----- self/ui/components/confirmation-modal.js | 102 -- self/ui/components/diff-viewer-ui.js | 720 ----------- self/ui/components/hitl-widget.js | 205 --- self/ui/components/toast-notifications.js | 122 -- self/ui/dashboard/metrics-dashboard.js | 456 ------- self/ui/dashboard/ui-manager.js | 127 -- self/ui/dashboard/vfs-explorer.js | 1114 ----------------- self/ui/panels/metrics-panel.js | 48 - self/ui/proto.js | 6 - self/ui/proto/schemas.js | 114 -- server/pool/policy-router.js | 87 +- tests/e2e/reploid-lab-helpers.js | 2 +- tests/integration/doppler-arena.test.js | 530 -------- tests/integration/long-session.test.js | 1 - tests/unit/confirmation-modal.test.js | 319 ----- tests/unit/critical-user-journeys.test.js | 62 + tests/unit/genesis-integrity.test.js | 2 +- .../unit/pool-critical-user-journeys.test.js | 51 + tests/unit/surface-claim-index.test.js | 12 +- tests/unit/toast-notifications.test.js | 306 ----- 84 files changed, 2206 insertions(+), 8140 deletions(-) rename TODO_REPLOID.md => docs/archive/reploid-descriptor-peer-orchestration.md (95%) create mode 100644 docs/critical-user-journeys.md create mode 100644 docs/poolday/critical-user-journeys.md create mode 100644 docs/status/poolday-critical-user-journeys.json create mode 100644 docs/status/x-critical-user-journeys.json create mode 100644 docs/status/zero-critical-user-journeys.json create mode 100644 docs/x/critical-user-journeys.md create mode 100644 docs/zero/critical-user-journeys.md create mode 100644 scripts/critical-user-journey-contract.js create mode 100644 scripts/verify-critical-user-journeys.js create mode 100644 scripts/verify-pool-critical-user-journeys.js create mode 100644 scripts/verify-x-critical-user-journeys.js create mode 100644 scripts/verify-zero-critical-user-journeys.js delete mode 100644 self/blueprints/implementation-status.md delete mode 100644 self/config/lab-route-profiles.js delete mode 100644 self/core/async-utils.js delete mode 100644 self/pool/TODO.md delete mode 100644 self/pool/layer-scheduler.js delete mode 100644 self/pool/peer-registry.js create mode 100644 self/pool/policy-validation.js delete mode 100644 self/pool/shard-negotiation.js delete mode 100644 self/styles/landing-mono.css delete mode 100644 self/styles/proto/hitl.css delete mode 100644 self/styles/vfs-explorer.css delete mode 100644 self/testing/arena/doppler-integration.js delete mode 100644 self/testing/arena/index.js delete mode 100644 self/ui/UI.js delete mode 100644 self/ui/boot-wizard/steps/detect.js delete mode 100644 self/ui/boot-wizard/zero-function.js delete mode 100644 self/ui/capsule/index.js delete mode 100644 self/ui/components/arena-results.js delete mode 100644 self/ui/components/confirmation-modal.js delete mode 100644 self/ui/components/diff-viewer-ui.js delete mode 100644 self/ui/components/hitl-widget.js delete mode 100644 self/ui/components/toast-notifications.js delete mode 100644 self/ui/dashboard/metrics-dashboard.js delete mode 100644 self/ui/dashboard/ui-manager.js delete mode 100644 self/ui/dashboard/vfs-explorer.js delete mode 100644 self/ui/panels/metrics-panel.js delete mode 100644 self/ui/proto.js delete mode 100644 self/ui/proto/schemas.js delete mode 100644 tests/integration/doppler-arena.test.js delete mode 100644 tests/unit/confirmation-modal.test.js create mode 100644 tests/unit/critical-user-journeys.test.js create mode 100644 tests/unit/pool-critical-user-journeys.test.js delete mode 100644 tests/unit/toast-notifications.test.js diff --git a/README.md b/README.md index 28e2d3030..6459822cf 100644 --- a/README.md +++ b/README.md @@ -4,15 +4,18 @@ [![License metadata: MIT](https://img.shields.io/badge/license%20metadata-MIT-blue.svg)](package.json) Reploid is a browser runtime family for receipt-backed, self-modifying agents. -Everything runs client-side; agent behavior stays in visible self files, prompts, -blueprints, traces, and receipts, never on a server. +Model execution and agent behavior stay in browser-visible self files, prompts, +blueprints, traces, and receipts. Optional server services provide authentication, +configuration, metadata relay, and hosted compatibility paths; the Poolday relay +rejects inference payloads. It ships as **three distinct surfaces** — Poolday, Zero, and X — each with its own route, boot profile, and tool set. They are separate products with separate authority: a capability supported on one surface is **not** implied on another. Every support claim is machine-checked in the [surface claim index](docs/status/surface-claim-index.json); read each row by its -declared boundary and status. +declared boundary and status. Current user outcomes and remaining work are owned +by the three registries in [Critical User Journeys](docs/critical-user-journeys.md). ## Surfaces @@ -51,7 +54,11 @@ Open `http://localhost:8000`. For the managed Gemini path, set `GEMINI_API_KEY` ## Self contract -Awaken clears prior live VFS state, writes the generated self manifests, exposes canonical source through a copy-on-write `/self` overlay, mounts Capsule, and starts the runtime. +Zero and X hydrate route-specific manifests into an instance-scoped VFS, expose +canonical source through a copy-on-write `/self` overlay, mount their respective +runtime UI, and start the agent loop. Writable Shadow and artifact state can +survive reload; live changes require Zero's explicit activated capabilities or +X's promotion path. The generated [VFS manifest](self/config/vfs-manifest.json) enumerates seeded files. The executable [tool-surface contract](self/config/tool-surfaces.js) enumerates tool membership. The [RGR runtime contract](self/blueprints/rgr-runtime-contract.md) defines candidate evidence, anchors, quarantine, rollback, and promotion. @@ -76,7 +83,8 @@ Users can bypass the managed access-window path and supply their own browser inf | --- | --- | | Operators | [Quick start](docs/QUICK-START.md), [configuration](docs/CONFIGURATION.md), and [local models](docs/local-models.md) | | Agent and runtime contributors | [System architecture](docs/system-architecture.md), [RGR runtime contract](self/blueprints/rgr-runtime-contract.md), and [tool surfaces](self/config/tool-surfaces.js) | -| Security and claim reviewers | [Security model](docs/SECURITY.md), [surface claim index](docs/status/surface-claim-index.json), [Poolday claims](docs/poolday/claims-and-nonclaims.md), and [threat model](docs/poolday/threat-model.md) | +| Product and release reviewers | [Critical user journeys](docs/critical-user-journeys.md), the three [machine-readable journey registries](docs/status/surface-claim-index.json), and the [surface claim index](docs/status/surface-claim-index.json) | +| Security and claim reviewers | [Security model](docs/SECURITY.md), [Poolday claims](docs/poolday/claims-and-nonclaims.md), and [threat model](docs/poolday/threat-model.md) | | Inference integrators | [Browser inference pool](docs/browser-inference-pool.md), [receipt schema](docs/poolday/receipt-schema.md), and [Doppler](https://github.com/clocksmith/doppler) | The [documentation index](docs/INDEX.md) owns the complete architecture, blueprint, API, and operator inventory. diff --git a/docs/API.md b/docs/API.md index 2675e9080..a50c040f6 100644 --- a/docs/API.md +++ b/docs/API.md @@ -123,8 +123,6 @@ Treat experimental paths as implementation details until they move into the main | Module | Path | Purpose | |--------|------|---------| | Proto UI | `self/ui/proto/index.js` | Main operator UI | -| UIManager | `self/ui/dashboard/ui-manager.js` | Dashboard orchestration | -| VFSExplorer | `self/ui/dashboard/vfs-explorer.js` | File tree UI | --- @@ -182,7 +180,7 @@ Important runtime storage paths: - `NeuralCompiler` currently lives under `self/experimental/intelligence/`, not `self/capabilities/intelligence/`. - There is no standalone runtime module at `self/infrastructure/introspector.js`. - There is no standalone runtime module at `self/core/sentinel-fsm.js`; the current reference is the blueprint `self/blueprints/0x000050-sentinel-fsm.md`. -- Older docs may refer to `ui/diff-generator.js`; the current UI surface is centered on `self/ui/proto/` and `self/ui/dashboard/`. +- Older docs may refer to `ui/diff-generator.js`; the current operator UI lives under `self/ui/proto/`. --- diff --git a/docs/INDEX.md b/docs/INDEX.md index c93e6b9d2..4bf917a42 100644 --- a/docs/INDEX.md +++ b/docs/INDEX.md @@ -33,6 +33,13 @@ Guide to all documentation in the REPLOID project. ### Reference - **[docs/API.md](./API.md)** - Module API documentation - **[docs/status/surface-claim-index.json](./status/surface-claim-index.json)** - Machine-checked surface status, evidence, blockers, and claim permission +- **[docs/critical-user-journeys.md](./critical-user-journeys.md)** - Canonical index for Poolday, Zero, and X user outcomes +- **[docs/status/poolday-critical-user-journeys.json](./status/poolday-critical-user-journeys.json)** - Canonical Poolday user outcomes, prerequisites, evidence, limitations, and remaining work +- **[docs/poolday/critical-user-journeys.md](./poolday/critical-user-journeys.md)** - Human-readable Poolday journey status +- **[docs/status/zero-critical-user-journeys.json](./status/zero-critical-user-journeys.json)** - Canonical Zero user outcomes, evidence, limitations, and remaining work +- **[docs/zero/critical-user-journeys.md](./zero/critical-user-journeys.md)** - Human-readable Zero journey status +- **[docs/status/x-critical-user-journeys.json](./status/x-critical-user-journeys.json)** - Canonical X user outcomes, evidence, limitations, and remaining work +- **[docs/x/critical-user-journeys.md](./x/critical-user-journeys.md)** - Human-readable X journey status - **[docs/browser-inference-pool.md](./browser-inference-pool.md)** - Poolday docs/internal contract for the public Reploid browser inference surface - **[docs/poolday/claims-and-nonclaims.md](./poolday/claims-and-nonclaims.md)** - Poolday claim boundary - **[docs/poolday/threat-model.md](./poolday/threat-model.md)** - Poolday adversaries, trust boundaries, and evidence @@ -46,6 +53,7 @@ Guide to all documentation in the REPLOID project. - **[docs/multi-model-evaluation.md](./multi-model-evaluation.md)** - Multi-model evaluation harness - **[docs/intent-bundle-lora.md](./intent-bundle-lora.md)** - Intent bundle LoRA workflow - **[docs/trained-adapter-promotion.md](./trained-adapter-promotion.md)** - Tinker adapter evidence, Shadow staging, and human-only promotion +- **[docs/archive/reploid-descriptor-peer-orchestration.md](./archive/reploid-descriptor-peer-orchestration.md)** - Archived layer-pipelining research; not the current Poolday execution contract - **[docs/CONFIGURATION.md](./CONFIGURATION.md)** - Boot UI settings and localStorage keys - **[docs/local-models.md](./local-models.md)** - WebLLM and Ollama setup - **[docs/style-guide.md](./style-guide.md)** - Code and UI conventions diff --git a/docs/TESTING.md b/docs/TESTING.md index 3634482b5..6de6324fa 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -49,12 +49,12 @@ Isolated tests for individual modules with mocked dependencies. -**34 test files covering:** +Representative test areas include: - Infrastructure: `di-container.test.js`, `event-bus.test.js`, `context-manager.test.js` - Core modules: `state-manager.test.js`, `utils.test.js`, `utils-core.test.js`, `vfs-module-loader.test.js` - Tools: `tools/read-file.test.js`, `tools/write-file.test.js`, `tools/edit-file.test.js`, `tools/list-files.test.js`, `tools/grep.test.js`, `tools/find.test.js` - Execution: `tool-runner.test.js`, `response-parser.test.js`, `schema-validator.test.js` -- UI/UX: `confirmation-modal.test.js`, `toast.test.js`, `toast-notifications.test.js` +- UI/UX: `toast.test.js`, `doppler-optimization-ui.test.js`, `design-system-css.test.js` - Capabilities: `audit-logger.test.js`, `rate-limiter.test.js`, `rule-engine.test.js`, `policy-engine.test.js`, `gepa-engines.test.js`, `episodic-memory.test.js`, `hybrid-retrieval.test.js`, `knowledge-tree.test.js`, `observability.test.js`, `verification-manager.test.js` - Browser: `browser-apis.test.js`, `core-hardening.test.js` - Networking: `webrtc-swarm.test.js`, `swarm-sync.test.js` @@ -76,7 +76,7 @@ Multi-module workflow tests with realistic state management. - Memory & state: `prompt-memory.test.js`, `reflection-system.test.js`, `replay-engine.test.js`, `vfs.test.js` - Persistence: `long-session.test.js`, `genesis-snapshot.test.js` - Tool execution: `tool-runner.test.js` -- Arena (safety): `arena-harness.test.js`, `doppler-arena.test.js` +- Arena (safety): `arena-harness.test.js` - Networking: `webrtc-swarm.test.js` - Routing: `websocket-routing.test.js` diff --git a/TODO_REPLOID.md b/docs/archive/reploid-descriptor-peer-orchestration.md similarity index 95% rename from TODO_REPLOID.md rename to docs/archive/reploid-descriptor-peer-orchestration.md index 6016b858a..955d29c65 100644 --- a/TODO_REPLOID.md +++ b/docs/archive/reploid-descriptor-peer-orchestration.md @@ -1,7 +1,7 @@ -# TODO: Reploid Descriptor Peer Orchestration +# Archived Reploid Descriptor Peer Orchestration Proposal > [!CAUTION] -> **Archived research, not the active Reploid plan.** This layer-pipelining proposal predates the shipped whole-job peer-room product and is not a release checklist. The canonical product contract and remaining work live in [`self/pool/pool-config.json`](./self/pool/pool-config.json) and [`self/pool/TODO.md`](./self/pool/TODO.md). Nothing below authorizes public claims that Reploid currently distributes individual model layers. +> **Archived research, not the active Reploid plan.** This layer-pipelining proposal predates the shipped whole-job peer-room product and is not a release checklist. The canonical product contract lives in [`../../self/pool/pool-config.json`](../../self/pool/pool-config.json), and current outcomes plus remaining work live in the [Poolday critical user journey registry](../status/poolday-critical-user-journeys.json). Nothing below authorizes public claims that Reploid currently distributes individual model layers. > [!IMPORTANT] > **Primary Directive:** Browser-Native Distributed Inference diff --git a/docs/browser-inference-pool.md b/docs/browser-inference-pool.md index 6119454cf..3dfb93396 100644 --- a/docs/browser-inference-pool.md +++ b/docs/browser-inference-pool.md @@ -15,11 +15,15 @@ Do not describe this as trustless compute, hardware-attested inference, or guara The target retrieval extension is documented in [Poolday Receipt-Backed Retrieval](./poolday/receipt-backed-retrieval.md). That document covers embeddings, vector indexes, query receipts, reranking receipts, and the competitive retrieval strategy. It is not a replacement for this current browser inference claim. +Current product outcomes, prerequisites, limitations, evidence, and remaining +work are tracked in [Poolday Critical User Journeys](./poolday/critical-user-journeys.md). +Architecture sections below explain mechanisms; they do not own completion +status. + The biological-sequence extension is documented in [Poolday Biological -Sequence Lane](./poolday/biological-sequence-lane.md). Its peer protocol and -synthetic runtime tests exist, but no biological model is enabled in the -Poolday catalog until immutable hosted artifacts and a matching Doppler release -are qualified. +Sequence Lane](./poolday/biological-sequence-lane.md). ESM-2 35M is enabled for +explicitly public protein pooled embeddings. The sequence lane is not biological +interpretation, medical advice, or a private-sequence service. Participation modes, device-root and passkey identity, adapter authority, route selection, and the model-shard boundary are specified in [Poolday Participation, diff --git a/docs/critical-user-journeys.md b/docs/critical-user-journeys.md new file mode 100644 index 000000000..001f18b5c --- /dev/null +++ b/docs/critical-user-journeys.md @@ -0,0 +1,27 @@ +# Reploid Critical User Journeys + +Reploid has three separate browser surfaces. Current product status is owned by +one machine-readable journey registry per surface: + +| Surface | Route | Readable summary | Canonical registry | +| --- | --- | --- | --- | +| Poolday | `/`, `/ask`, `/compute`, `/records` | [Poolday journeys](./poolday/critical-user-journeys.md) | [`poolday-critical-user-journeys.json`](./status/poolday-critical-user-journeys.json) | +| Zero | `/zero` | [Zero journeys](./zero/critical-user-journeys.md) | [`zero-critical-user-journeys.json`](./status/zero-critical-user-journeys.json) | +| X | `/x` | [X journeys](./x/critical-user-journeys.md) | [`x-critical-user-journeys.json`](./status/x-critical-user-journeys.json) | + +The registries own journey status, prerequisites, executable implementation +paths, tests, limitations, release-evidence requirements, and remaining work. +Architecture documents and blueprints may explain mechanisms, but they do not +advance completion status. + +Run `npm run verify:journeys` to validate all three registries. The validator +fails on missing evidence paths, unlinked work, invalid status, uncovered +routes, or a missing release gate. Poolday additionally verifies that every +journey model is enabled and permitted by every policy named by that journey. + +`Supported` means an executable outcome with automated end-to-end contract +coverage. It does not mean every model, device, network, or objective works. +`Conditional` names those prerequisites. `Limited` names the narrower outcome +that works while the complete user expectation remains unproved. Deployed +claims also require a retained artifact; a passing test mentioned only in prose +does not satisfy that requirement. diff --git a/docs/multi-model-evaluation.md b/docs/multi-model-evaluation.md index 1b91dd0fc..919f6cb15 100644 --- a/docs/multi-model-evaluation.md +++ b/docs/multi-model-evaluation.md @@ -7,7 +7,6 @@ Multi-model evaluation runs the same task suite across multiple model configs an ## Module **Path:** `core/multi-model-evaluator.js` -**Capability shim:** `capabilities/intelligence/multi-model-evaluator.js` **Primary API:** `evaluate(tasks, modelConfigs, options)` diff --git a/docs/poolday/critical-user-journeys.md b/docs/poolday/critical-user-journeys.md new file mode 100644 index 000000000..6829e4fb5 --- /dev/null +++ b/docs/poolday/critical-user-journeys.md @@ -0,0 +1,43 @@ +# Poolday Critical User Journeys + +Poolday is the internal name for the public Reploid browser-inference surface at +`/`. The canonical journey registry is +[`../status/poolday-critical-user-journeys.json`](../status/poolday-critical-user-journeys.json). +It owns current user outcomes, prerequisites, status, implementation evidence, +tests, limitations, and remaining work. + +## Current journey status + +| Journey | Status | Honest outcome | +| --- | --- | --- | +| Request a text answer | Conditional | A compatible browser can answer one prompt and return a signed receipt. | +| Recover with local inference | Conditional | After explicit consent, a qualified browser can load the model and retry the preserved request. | +| Contribute browser compute | Conditional | A qualified tab can load one complete model, advertise, answer, sign, and stop. | +| Verify receipt agreement | Supported | The requester can require and inspect deterministic signed-receipt agreement. | +| Inspect records | Limited | Answers, contributions, room events, and scores persist in this browser and room. | +| Run a public protein embedding | Conditional | ESM-2 can return a receipt-bound pooled embedding for an explicitly public sequence. | +| Run a published adapter | Conditional | A promoted, compatible, fetchable adapter can be approved and bound into a receipt. | +| Earn protocol reputation | Limited | Accepted work creates signed local points and reputation events. | +| Receive paid compensation | Blocked | No monetary settlement system exists. | + +`Conditional` is not a euphemism for supported everywhere. The registry names the +provider, device, artifact, network, registry, and privacy prerequisites for each +journey. `Limited` identifies the narrower outcome that works today. + +## Updating status + +A journey change is complete only when the same registry entry contains: + +1. A user-visible outcome and its prerequisites. +2. Executable implementation paths. +3. Automated tests for success and relevant failure recovery. +4. Honest limitations and claim boundaries. +5. A release gate and, for deployed claims, a retained run artifact. + +The release artifact must bind its journey ids, commit, deployment URL, pool +configuration identity, timestamps, browser, model, receipt, agreement, and final +result. A passing command reported only in prose does not advance journey status. + +Remaining work belongs in the registry's `openWork` collection and must reference +one or more journeys. Architecture documents may explain a design, but they do not +own completion status. diff --git a/docs/poolday/participation-identity-routing.md b/docs/poolday/participation-identity-routing.md index 149137c22..73ed03b3d 100644 --- a/docs/poolday/participation-identity-routing.md +++ b/docs/poolday/participation-identity-routing.md @@ -72,9 +72,8 @@ a different route hash cannot enter agreement. Every provider still loads and runs the complete selected model. Model manifests and shards come from pinned artifact hosting and are cached locally by Doppler. -The shard-negotiation module can verify that a provider has an exact manifest -and shard set before dispatch, but the current live peer-room path does not -relay base-model shards between browsers. Adapter chunks do have a verified -peer-transfer path. Poolday does not claim tensor, layer, attention, or KV-cache -sharding. - +The active runtime verifies the loaded manifest identity and artifact hashes +before provider registration and assignment. The current live peer-room path +does not relay base-model shards between browsers. Adapter chunks do have a +verified peer-transfer path. Poolday does not claim tensor, layer, attention, +or KV-cache sharding. diff --git a/docs/status/poolday-critical-user-journeys.json b/docs/status/poolday-critical-user-journeys.json new file mode 100644 index 000000000..ac12d2984 --- /dev/null +++ b/docs/status/poolday-critical-user-journeys.json @@ -0,0 +1,452 @@ +{ + "schema": "reploid/poolday-critical-user-journeys/v1", + "updated": "2026-07-27", + "surface": "/", + "publicProductName": "Reploid", + "internalSurfaceName": "Poolday", + "statusDefinitions": { + "supported": "The outcome has an executable implementation and automated end-to-end contract coverage.", + "conditional": "The outcome is executable only when named device, artifact, provider, network, or registry prerequisites are satisfied.", + "limited": "A narrower local or protocol outcome works, but the broader user expectation is not yet met.", + "blocked": "The user outcome is intentionally unavailable and has explicit blockers." + }, + "releaseEvidence": { + "gate": "scripts/verify-pool-release.js", + "retainedArtifact": null, + "status": "not-retained", + "requiredArtifactFields": [ + "journeyIds", + "commit", + "deploymentUrl", + "configVersion", + "configHash", + "startedAt", + "completedAt", + "browserChannel", + "modelId", + "receiptHash", + "agreementHash", + "result" + ] + }, + "journeys": [ + { + "id": "request-text-answer", + "actor": "requester", + "outcome": "Send one text prompt to a compatible browser contributor and receive an answer with an inspectable signed receipt.", + "routes": [ + "/", + "/ask" + ], + "status": "conditional", + "releaseCritical": true, + "prerequisites": [ + "A compatible contributor advertises the exact requested model and runtime profile.", + "The requester and contributor establish a WebRTC DataChannel.", + "The prompt passes the public-provider policy classes." + ], + "modelIds": [ + "gemma-3-270m-it-q4k-ehf16-af32", + "qwen-3-5-0-8b-q4k-ehaf16", + "gemma-4-e2b-it-q4k-ehf16-af32-int4ple" + ], + "policyIds": [ + "fastest_receipt", + "canary_audited", + "redundant_agreement", + "ring_quorum_receipt" + ], + "implementationPaths": [ + "self/ui/pool-home/controls.js", + "self/pool/requester-client.js", + "self/pool/peer-room.js" + ], + "testPaths": [ + "tests/unit/pool-home-ask-controls.test.js", + "tests/unit/pool-peer-room.test.js", + "scripts/pool-actual-browser-smoke.js" + ], + "limitations": [ + "There is no hosted inference fallback.", + "The default generation contract is a one-shot deterministic response capped at 128 output tokens.", + "Provider availability and answer quality are not established by protocol correctness alone." + ], + "openWorkIds": [ + "retain-deployed-journey-evidence", + "measure-provider-availability", + "gate-text-answer-quality", + "capture-stale-peer-cleanup-evidence", + "build-serverless-wide-area-peer-graph" + ] + }, + { + "id": "recover-with-local-inference", + "actor": "requester", + "outcome": "Preserve a request when no contributor is available and, after explicit consent, download the model and run it on this browser.", + "routes": [ + "/", + "/ask" + ], + "status": "conditional", + "releaseCritical": true, + "prerequisites": [ + "The device qualifies for the selected model's WebGPU requirements.", + "The pinned model artifacts are reachable and fit browser storage.", + "The user explicitly chooses Download and run here." + ], + "implementationPaths": [ + "self/ui/pool-home/controls.js", + "self/pool/doppler-runtime.js", + "self/pool/peer-room.js" + ], + "testPaths": [ + "tests/unit/pool-home-ask-controls.test.js", + "tests/unit/pool-doppler-runtime.test.js" + ], + "limitations": [ + "The first run may require a large model download.", + "Unsupported browsers remain request-only." + ], + "openWorkIds": [ + "retain-deployed-journey-evidence" + ] + }, + { + "id": "contribute-browser-compute", + "actor": "contributor", + "outcome": "Qualify this browser, load one complete model, advertise availability, answer compatible jobs, sign receipts, and stop sharing at any time.", + "routes": [ + "/", + "/compute" + ], + "status": "conditional", + "releaseCritical": true, + "prerequisites": [ + "The device qualifies for the selected model.", + "The model artifacts load through Doppler and browser storage.", + "The tab remains open and reachable in the selected room." + ], + "implementationPaths": [ + "self/ui/pool-home/controls.js", + "self/pool/provider-client.js", + "self/pool/peer-room.js" + ], + "testPaths": [ + "tests/unit/pool-home-ask-controls.test.js", + "tests/unit/pool-peer-room.test.js", + "scripts/pool-actual-browser-smoke.js" + ], + "limitations": [ + "Each contributor loads and executes the complete model; model layers and KV state are not distributed across peers.", + "Keeping a contributor tab available is voluntary and creates no uptime guarantee." + ], + "openWorkIds": [ + "retain-deployed-journey-evidence", + "measure-provider-availability", + "capture-stale-peer-cleanup-evidence", + "recover-hosted-diagnostic-assignments", + "build-serverless-wide-area-peer-graph" + ] + }, + { + "id": "verify-receipt-agreement", + "actor": "requester", + "outcome": "Choose a signed, canary-audited, redundant, or adaptive ring policy and inspect whether the required deterministic receipts agree.", + "routes": [ + "/", + "/ask", + "/records" + ], + "status": "supported", + "releaseCritical": true, + "prerequisites": [ + "Enough compatible contributors are available for the selected policy.", + "Compared contributors share the required exact model and runtime profile." + ], + "implementationPaths": [ + "self/pool/peer-control-plane.js", + "self/pool/peer-planning.js", + "self/pool/peer-room.js", + "self/pool/inference-receipt.js" + ], + "testPaths": [ + "tests/unit/pool-peer-control-plane.test.js", + "tests/unit/pool-peer-room.test.js", + "tests/unit/poolday-audit.test.js" + ], + "limitations": [ + "Agreement proves matching signed artifacts, not factual correctness, honest GPU execution, or absence of collusion.", + "A one-provider policy supplies accountability but no independent comparison." + ], + "openWorkIds": [ + "retain-deployed-journey-evidence" + ] + }, + { + "id": "inspect-local-records", + "actor": "requester_or_contributor", + "outcome": "Inspect this browser's saved answers, contribution receipts, room activity, scores, and technical receipt details.", + "routes": [ + "/records", + "/history", + "/network" + ], + "status": "limited", + "releaseCritical": true, + "prerequisites": [ + "The browser permits localStorage for local records.", + "Authenticated coordinator access is available for hosted receipt lookup." + ], + "implementationPaths": [ + "self/ui/pool-home/record-persistence.js", + "self/ui/pool-home/view.js", + "self/ui/pool-home/controls.js" + ], + "testPaths": [ + "tests/unit/pool-home-record.test.js" + ], + "limitations": [ + "Primary peer-room records are local to the browser and room.", + "History and network are compatibility aliases for records, not separate products.", + "There is no account-synchronized cross-device history." + ], + "openWorkIds": [ + "add-durable-cross-device-records" + ] + }, + { + "id": "run-public-protein-embedding", + "actor": "requester", + "outcome": "Send an explicitly public protein sequence to a selected ESM-2 contributor and receive a receipt-bound pooled embedding.", + "routes": [ + "/" + ], + "status": "conditional", + "releaseCritical": true, + "prerequisites": [ + "The user confirms the sequence is public.", + "A compatible ESM-2 contributor is available or this browser can load ESM-2.", + "The sequence satisfies the amino-acid and length contract." + ], + "modelIds": [ + "esm2-t12-35m-ur50d-f32-af32" + ], + "policyIds": [ + "fastest_receipt", + "canary_audited", + "redundant_agreement", + "ring_quorum_receipt" + ], + "implementationPaths": [ + "self/ui/pool-home/controls.js", + "self/pool/sequence-workload.js", + "self/pool/peer-room.js" + ], + "testPaths": [ + "tests/unit/pool-home-ask-controls.test.js", + "tests/unit/pool-sequence-workload.test.js", + "tests/unit/pool-peer-room.test.js" + ], + "limitations": [ + "Private or sensitive biological sequences are not accepted.", + "The product returns an embedding, not biological interpretation or medical advice." + ], + "openWorkIds": [ + "retain-deployed-journey-evidence" + ] + }, + { + "id": "run-published-adapter", + "actor": "requester", + "outcome": "Select a promoted adapter pack for an exact base model and receive a receipt binding the pack, approval, acquisition, and inference result.", + "routes": [ + "/", + "/ask", + "/compute" + ], + "status": "conditional", + "releaseCritical": false, + "prerequisites": [ + "A promoted public adapter publication matches the exact selected base model.", + "The contributor can fetch, verify, and load the adapter bytes." + ], + "implementationPaths": [ + "self/pool/adapter-registry.js", + "self/pool/adapter-publication.js", + "self/ui/pool-home/controls.js" + ], + "testPaths": [ + "tests/unit/pool-home-ask-controls.test.js", + "tests/unit/pool-adapter-publication.test.js", + "tests/unit/pool-peer-room.test.js" + ], + "limitations": [ + "The adapter lane is hidden when no compatible promoted publication is available.", + "A published pack proves custody and compatibility, not task quality." + ], + "openWorkIds": [ + "retain-adapter-supply-evidence" + ] + }, + { + "id": "earn-protocol-reputation", + "actor": "contributor", + "outcome": "Receive signed points and reputation events after the requester accepts a valid receipt or receipt agreement.", + "routes": [ + "/compute", + "/records" + ], + "status": "limited", + "releaseCritical": true, + "prerequisites": [ + "A requester accepts the contributor's valid receipt.", + "The participating peers retain the signed ledger events." + ], + "implementationPaths": [ + "self/pool/peer-control-plane.js", + "self/pool/peer-ledger.js", + "self/ui/pool-home/record-persistence.js" + ], + "testPaths": [ + "tests/unit/pool-peer-control-plane.test.js", + "tests/unit/pool-peer-room.test.js", + "tests/unit/pool-home-record.test.js" + ], + "limitations": [ + "Peer-room points and reputation are local replicated protocol events, not money.", + "There is no globally durable, Sybil-resistant reputation network." + ], + "openWorkIds": [ + "add-durable-cross-device-records", + "build-serverless-wide-area-peer-graph" + ] + }, + { + "id": "receive-paid-compensation", + "actor": "contributor", + "outcome": "Receive monetary compensation for accepted browser inference work.", + "routes": [ + "/compute" + ], + "status": "blocked", + "releaseCritical": false, + "prerequisites": [], + "implementationPaths": [], + "testPaths": [], + "limitations": [ + "No payment, pricing, payout, tax, dispute, or settlement system exists." + ], + "openWorkIds": [ + "design-paid-settlement" + ] + } + ], + "openWork": [ + { + "id": "retain-deployed-journey-evidence", + "state": "open", + "priority": "P0", + "journeyIds": [ + "request-text-answer", + "recover-with-local-inference", + "contribute-browser-compute", + "verify-receipt-agreement", + "run-public-protein-embedding" + ], + "summary": "Persist every actual-browser release run as an immutable journey artifact instead of recording a checked box.", + "acceptance": "The release gate writes all releaseEvidence.requiredArtifactFields to a repository-governed or content-addressed artifact and exposes its path from this registry." + }, + { + "id": "measure-provider-availability", + "state": "open", + "priority": "P0", + "journeyIds": [ + "request-text-answer", + "contribute-browser-compute" + ], + "summary": "Measure whether a requester can actually find a compatible contributor and reach a receipt.", + "acceptance": "A retained production report records discovery success, connection success, receipt success, no-provider recovery, and latency separately." + }, + { + "id": "gate-text-answer-quality", + "state": "open", + "priority": "P0", + "journeyIds": [ + "request-text-answer" + ], + "summary": "Evaluate whether enabled text models produce useful visible answers for the prompts the UI suggests.", + "acceptance": "A versioned prompt set has machine checks and human recognizability or usefulness verdicts bound to model, build, receipt, and output." + }, + { + "id": "add-durable-cross-device-records", + "state": "open", + "priority": "P1", + "journeyIds": [ + "inspect-local-records", + "earn-protocol-reputation" + ], + "summary": "Provide durable, identity-bound records without misrepresenting localStorage as synchronized history.", + "acceptance": "An authenticated user can recover accepted receipts and reputation events on a second browser with signature and hash verification." + }, + { + "id": "retain-adapter-supply-evidence", + "state": "open", + "priority": "P1", + "journeyIds": [ + "run-published-adapter" + ], + "summary": "Track whether any promoted public adapter is presently fetchable and executable.", + "acceptance": "A retained production artifact binds publication, adapter bytes, exact base model, browser execution, receipt, and current availability." + }, + { + "id": "capture-stale-peer-cleanup-evidence", + "state": "open", + "priority": "P1", + "journeyIds": [ + "request-text-answer", + "contribute-browser-compute" + ], + "summary": "Capture deployed expiration and stale-peer cleanup behavior for relay and hosted signaling paths.", + "acceptance": "A deployed test proves expired adverts and signaling messages stop affecting discovery and assignment." + }, + { + "id": "recover-hosted-diagnostic-assignments", + "state": "open", + "priority": "P2", + "journeyIds": [ + "contribute-browser-compute" + ], + "summary": "Recover the optional hosted diagnostic flow after assignment expiration or a missed reveal.", + "acceptance": "Hosted diagnostic tests cover registration, assignment claim, commit, reveal, expiration, recovery, and terminal receipt state." + }, + { + "id": "build-serverless-wide-area-peer-graph", + "state": "open", + "priority": "P2", + "journeyIds": [ + "request-text-answer", + "contribute-browser-compute", + "earn-protocol-reputation" + ], + "summary": "Move remote discovery and event gossip beyond the optional Reploid room relay.", + "acceptance": "Two remote browsers discover, connect, accept a receipt, and converge ledger state without any Reploid server relay." + }, + { + "id": "design-paid-settlement", + "state": "blocked", + "priority": "P2", + "journeyIds": [ + "receive-paid-compensation" + ], + "summary": "Define pricing, payout, tax, dispute, abuse, and settlement authority before monetary rewards exist.", + "acceptance": "Product, legal, security, identity, accounting, and dispute contracts are approved and tested before any paid contribution claim." + } + ], + "constraints": [ + "Do not claim trustless, hardware-attested, or guaranteed honest browser execution.", + "The metadata relay must not carry prompts, outputs, token streams, model payloads, or full receipts.", + "Each contributor executes a complete model; Poolday does not claim distributed layer, attention, or KV-cache execution.", + "Local peer-room points are not money.", + "A journey status may advance only with executable evidence; narrative or a checked TODO item is insufficient." + ] +} diff --git a/docs/status/surface-claim-index.json b/docs/status/surface-claim-index.json index adc969fb0..6c0677550 100644 --- a/docs/status/surface-claim-index.json +++ b/docs/status/surface-claim-index.json @@ -1,6 +1,11 @@ { - "schema": "reploid/surface-claim-index/v1", - "updated": "2026-07-11", + "schema": "reploid/surface-claim-index/v2", + "updated": "2026-07-27", + "journeyRegistries": { + "/": "docs/status/poolday-critical-user-journeys.json", + "/zero": "docs/status/zero-critical-user-journeys.json", + "/x": "docs/status/x-critical-user-journeys.json" + }, "entries": [ { "surface": "/", @@ -9,6 +14,7 @@ "self/pool/pool-config.json", "tests/unit/pool-contract.test.js", "tests/unit/pool-routes.test.js", + "docs/status/poolday-critical-user-journeys.json", "docs/poolday/claims-and-nonclaims.md" ], "blockers": [], @@ -20,7 +26,8 @@ "evidencePaths": [ "self/config/surface-intents.js", "tests/unit/surface-intents.test.js", - "tests/e2e/boot.spec.js" + "tests/e2e/boot.spec.js", + "docs/status/zero-critical-user-journeys.json" ], "blockers": [], "claimPermission": true @@ -31,7 +38,8 @@ "evidencePaths": [ "self/config/surface-intents.js", "tests/unit/surface-intents.test.js", - "tests/e2e/boot.spec.js" + "tests/e2e/boot.spec.js", + "docs/status/x-critical-user-journeys.json" ], "blockers": [], "claimPermission": true diff --git a/docs/status/x-critical-user-journeys.json b/docs/status/x-critical-user-journeys.json new file mode 100644 index 000000000..6ed2e3ced --- /dev/null +++ b/docs/status/x-critical-user-journeys.json @@ -0,0 +1,460 @@ +{ + "schema": "reploid/x-critical-user-journeys/v1", + "updated": "2026-07-27", + "surface": "/x", + "productName": "X", + "statusDefinitions": { + "supported": "The outcome has an executable implementation and automated end-to-end contract coverage.", + "conditional": "The outcome is executable only when named model, device, artifact, provider, peer, or evidence prerequisites are satisfied.", + "limited": "A component or local protocol path works, but the full operator outcome is not yet proved.", + "blocked": "The user outcome is intentionally unavailable and has explicit blockers." + }, + "releaseEvidence": { + "gate": "scripts/verify-critical-user-journeys.js", + "retainedArtifact": null, + "status": "not-retained", + "requiredArtifactFields": [ + "journeyIds", + "commit", + "deploymentUrl", + "bootProfile", + "modelId", + "provider", + "startedAt", + "completedAt", + "cycleArtifactHashes", + "promotionReceiptHashes", + "exportHash", + "screenshotHash", + "result" + ] + }, + "journeys": [ + { + "id": "configure-and-awaken-x", + "actor": "operator", + "outcome": "Choose browser, direct, or proxy inference, set the first objective and cycle interval, and awaken the full X substrate with its governed tool surface.", + "routes": [ + "/x" + ], + "status": "conditional", + "releaseCritical": true, + "prerequisites": [ + "The chosen provider configuration is complete and its runtime is reachable.", + "The objective is non-empty.", + "The full VFS seed and required modules load successfully." + ], + "implementationPaths": [ + "self/ui/boot-home/index.js", + "self/ui/boot-wizard/state.js", + "self/host/start-app.js", + "self/lab/profiles.js", + "self/config/surface-intents.js" + ], + "testPaths": [ + "tests/e2e/boot.spec.js", + "tests/e2e/boot-contract.spec.js", + "tests/unit/genesis-integrity.test.js" + ], + "limitations": [ + "Configuration completeness can enable Awaken before a successful provider call.", + "A successful boot proves module and tool availability, not current model quality or the usefulness of every advanced subsystem." + ], + "openWorkIds": [ + "retain-x-release-evidence", + "prove-x-inference-preflight" + ] + }, + { + "id": "run-steer-pause-and-resume-x", + "actor": "operator", + "outcome": "Run an objective, inspect its timeline and tool results, inject a human message, stop the loop, resume it with the stored objective, and inspect status and telemetry.", + "routes": [ + "/x" + ], + "status": "conditional", + "releaseCritical": true, + "prerequisites": [ + "A configured inference provider returns parser-compatible responses.", + "The objective remains stored for resume." + ], + "implementationPaths": [ + "self/core/agent-loop.js", + "self/ui/proto/index.js", + "self/ui/components/inline-chat.js", + "self/ui/proto/telemetry.js" + ], + "testPaths": [ + "tests/e2e/iteration-artifacts.spec.js", + "tests/e2e/dashboard.spec.js", + "tests/e2e/agent-goals.spec.js" + ], + "limitations": [ + "Most deterministic iteration tests use mock cognition.", + "Existing browser tests prove the controls and timeline separately, not one complete inject, stop, resume, and settle journey.", + "A DONE state does not independently prove that the objective was solved correctly." + ], + "openWorkIds": [ + "retain-x-release-evidence", + "evaluate-x-goal-quality", + "prove-x-operator-control-loop" + ] + }, + { + "id": "inspect-edit-and-recover-x-workspace", + "actor": "operator", + "outcome": "Browse and search the VFS, inspect or edit writable files, preserve Shadow and artifact state across reload, and keep direct live-self writes behind the promotion boundary.", + "routes": [ + "/x" + ], + "status": "supported", + "releaseCritical": true, + "prerequisites": [ + "IndexedDB and the service worker remain available for the same X instance." + ], + "implementationPaths": [ + "self/ui/proto/vfs.js", + "self/core/vfs.js", + "self/config/vfs-policy.js", + "self/tools/WriteFile.js" + ], + "testPaths": [ + "tests/e2e/vfs-roundtrip.spec.js", + "tests/e2e/boot-replay.spec.js", + "tests/integration/vfs.test.js", + "tests/unit/tools/write-file.test.js" + ], + "limitations": [ + "Recovery is local to the browser instance.", + "The VFS editor does not convert an edited Shadow candidate into a safe live change by itself.", + "Browser persistence is not source-control history or a cross-device backup." + ], + "openWorkIds": [ + "prove-x-export-recovery" + ] + }, + { + "id": "evaluate-and-promote-x-candidate", + "actor": "agent_or_operator", + "outcome": "Evaluate a Shadow candidate in the arena, require byte-bound replay evidence, preserve rollback material, and promote an allowlisted target into the live self.", + "routes": [ + "/x" + ], + "status": "supported", + "releaseCritical": true, + "prerequisites": [ + "The candidate is under /shadow and the target is allowlisted under /self.", + "Evidence under /artifacts binds the requested paths and candidate bytes and records replayPassed true.", + "Validator and Clockwork targets satisfy their additional authority rules." + ], + "implementationPaths": [ + "self/testing/arena/arena-harness.js", + "self/tools/Promote.js", + "self/core/promotion-policy.js" + ], + "testPaths": [ + "tests/e2e/x-one-safe-candidate.spec.js", + "tests/e2e/minimal-candidate-harness.spec.js", + "tests/unit/tools/promote.test.js", + "tests/unit/clockwork-promotion-policy.test.js" + ], + "limitations": [ + "Promote is callable by the X agent as well as the optimization UI; ordinary allowlisted promotion is not a universal human-approval gate.", + "The arena and replay evidence do not prove arbitrary semantic correctness.", + "Validator targets are quarantined, but other allowlisted self targets can be promoted after the defined evidence checks pass." + ], + "openWorkIds": [ + "retain-x-release-evidence", + "make-x-promotion-authority-visible", + "evaluate-x-goal-quality" + ] + }, + { + "id": "optimize-and-activate-doppler-profile", + "actor": "operator", + "outcome": "Edit a governed Doppler optimization contract, run candidate profiles, inspect receipts and scores, promote the selected profile, canary it, and activate the exact accepted profile.", + "routes": [ + "/x" + ], + "status": "conditional", + "releaseCritical": false, + "prerequisites": [ + "Doppler tooling and the selected local model support the requested optimization contract.", + "Candidate runs finish and produce valid receipt-bound evidence.", + "Promote and the activation canary accept the same profile bytes and hash." + ], + "implementationPaths": [ + "self/ui/proto/optimization.js", + "self/capabilities/system/doppler-optimizer.js", + "self/tools/DopplerOptimize.js", + "self/tools/Promote.js" + ], + "testPaths": [ + "tests/unit/doppler-optimization-ui.test.js", + "tests/unit/doppler-optimizer.test.js", + "tests/unit/doppler-reploid-provider.test.js" + ], + "limitations": [ + "The UI and optimizer contracts have focused tests, but no retained deployed browser run proves a current model artifact completing the whole lane.", + "A better score on the declared contract does not establish general model improvement." + ], + "openWorkIds": [ + "retain-x-optimization-evidence" + ] + }, + { + "id": "delegate-and-monitor-x-workers", + "actor": "operator_or_agent", + "outcome": "Spawn restricted worker tasks, inspect active and completed workers, await results, and clear completed cards.", + "routes": [ + "/x" + ], + "status": "limited", + "releaseCritical": false, + "prerequisites": [ + "WorkerManager initializes with a compatible model configuration.", + "The requested worker type and allowed tools are present." + ], + "implementationPaths": [ + "self/core/worker-manager.js", + "self/tools/SpawnWorker.js", + "self/tools/AwaitWorkers.js", + "self/ui/proto/workers.js" + ], + "testPaths": [ + "tests/unit/worker-manager.test.js", + "tests/e2e/workers.spec.js" + ], + "limitations": [ + "The browser worker suite currently proves the panel and empty states, not a real spawned worker reaching a useful result through the UI.", + "Worker tool restrictions are runtime policy, not operating-system isolation." + ], + "openWorkIds": [ + "prove-x-worker-journey" + ] + }, + { + "id": "inspect-x-memory-and-cognition", + "actor": "operator", + "outcome": "Inspect memory summaries, compactions, retrieval events, knowledge, and cognition state produced by the running agent.", + "routes": [ + "/x" + ], + "status": "limited", + "releaseCritical": false, + "prerequisites": [ + "The run has produced memory, retrieval, or knowledge records.", + "The relevant cognition services initialize successfully." + ], + "implementationPaths": [ + "self/core/memory-manager.js", + "self/capabilities/cognition/semantic/semantic-memory-llm.js", + "self/capabilities/cognition/symbolic/knowledge-graph.js", + "self/ui/proto/index.js", + "self/ui/panels/cognition-panel.js" + ], + "testPaths": [ + "tests/integration/prompt-memory.test.js", + "tests/unit/episodic-memory.test.js", + "tests/unit/knowledge-tree.test.js", + "tests/e2e/dashboard.spec.js" + ], + "limitations": [ + "The visible panels prove inspectability, not that retrieval improves objective success.", + "No held-out evaluation currently measures memory relevance, contamination, or reuse for X journeys." + ], + "openWorkIds": [ + "evaluate-x-memory-value" + ] + }, + { + "id": "export-and-replay-x-run", + "actor": "operator", + "outcome": "Export state and VFS, import a run file into Replay, and play, pause, step, reset, and change replay speed while inspecting emitted events.", + "routes": [ + "/x" + ], + "status": "limited", + "releaseCritical": true, + "prerequisites": [ + "The browser permits file download and file selection.", + "The imported JSON satisfies the replay engine contract." + ], + "implementationPaths": [ + "self/ui/proto/index.js", + "self/ui/proto/replay.js", + "self/infrastructure/replay-engine.js" + ], + "testPaths": [ + "tests/integration/replay-engine.test.js", + "tests/e2e/dashboard.spec.js" + ], + "limitations": [ + "Current browser coverage proves that replay and export controls exist, not that an exported X run round-trips through the importer.", + "Replay re-emits recorded events; it does not rerun model inference or verify the original model output." + ], + "openWorkIds": [ + "prove-x-export-recovery", + "prove-x-replay-roundtrip" + ] + }, + { + "id": "share-x-files-with-peers", + "actor": "agent", + "outcome": "Discover X swarm peers and share or request permitted files through the WebRTC swarm tool surface.", + "routes": [ + "/x" + ], + "status": "limited", + "releaseCritical": false, + "prerequisites": [ + "Compatible peers join the same session-scoped room.", + "Cross-host peers have a reachable signaling path.", + "The requested file and operation satisfy swarm transport policy." + ], + "implementationPaths": [ + "self/capabilities/communication/webrtc-swarm.js", + "self/capabilities/communication/swarm-transport.js", + "self/tools/SwarmShareFile.js", + "self/tools/SwarmRequestFile.js" + ], + "testPaths": [ + "tests/integration/webrtc-swarm.test.js", + "tests/unit/webrtc-swarm.test.js", + "tests/unit/swarm-sync.test.js" + ], + "limitations": [ + "Component and protocol tests do not establish a currently available public peer network.", + "Cross-host WebRTC still requires signaling for rendezvous.", + "No retained remote two-browser X artifact proves file exchange on the deployed surface." + ], + "openWorkIds": [ + "prove-x-remote-swarm-journey" + ] + } + ], + "openWork": [ + { + "id": "retain-x-release-evidence", + "state": "open", + "priority": "P0", + "journeyIds": [ + "configure-and-awaken-x", + "run-steer-pause-and-resume-x", + "evaluate-and-promote-x-candidate" + ], + "summary": "Persist an actual deployed X run and promotion as immutable journey evidence.", + "acceptance": "A retained artifact supplies every releaseEvidence.requiredArtifactFields value and is linked from releaseEvidence.retainedArtifact." + }, + { + "id": "evaluate-x-goal-quality", + "state": "open", + "priority": "P0", + "journeyIds": [ + "run-steer-pause-and-resume-x", + "evaluate-and-promote-x-candidate" + ], + "summary": "Evaluate whether X completes representative objectives correctly rather than merely advancing cycles or accepting its own declared evidence.", + "acceptance": "A versioned objective set binds model, trace, tool calls, mutations, independent checks, final artifacts, and human usefulness verdicts." + }, + { + "id": "make-x-promotion-authority-visible", + "state": "open", + "priority": "P0", + "journeyIds": [ + "evaluate-and-promote-x-candidate" + ], + "summary": "Make the actor, evidence, quarantine, rollback, and approval authority of each promotion visible before live activation.", + "acceptance": "The UI distinguishes agent-requested, operator-requested, externally authorized, quarantined, rejected, and accepted promotion, and a reviewer can inspect the bound evidence before activation." + }, + { + "id": "prove-x-inference-preflight", + "state": "open", + "priority": "P1", + "journeyIds": [ + "configure-and-awaken-x" + ], + "summary": "Prevent a syntactically complete but unreachable inference configuration from appearing ready.", + "acceptance": "Awaken readiness distinguishes configured from verified, with tested failure and recovery for browser, direct, and proxy paths." + }, + { + "id": "prove-x-operator-control-loop", + "state": "open", + "priority": "P1", + "journeyIds": [ + "run-steer-pause-and-resume-x" + ], + "summary": "Exercise X's user controls as one coherent run rather than testing control visibility separately.", + "acceptance": "Browser coverage injects a message, observes it in the next model input, stops, resumes, reaches a terminal state, and verifies timeline, status, and telemetry consistency." + }, + { + "id": "retain-x-optimization-evidence", + "state": "open", + "priority": "P1", + "journeyIds": [ + "optimize-and-activate-doppler-profile" + ], + "summary": "Retain a deployed browser optimization, promotion, canary, and activation chain for a current Doppler artifact.", + "acceptance": "One immutable artifact binds contract, candidates, scores, receipt hashes, selected profile bytes, promotion, canary rerun, and active profile identity." + }, + { + "id": "prove-x-worker-journey", + "state": "open", + "priority": "P1", + "journeyIds": [ + "delegate-and-monitor-x-workers" + ], + "summary": "Prove a real restricted X worker completes useful work visible to the operator.", + "acceptance": "An end-to-end browser test spawns a worker, shows progress, enforces its tool policy, awaits a result, renders completion, and clears it." + }, + { + "id": "evaluate-x-memory-value", + "state": "open", + "priority": "P1", + "journeyIds": [ + "inspect-x-memory-and-cognition" + ], + "summary": "Measure whether X memory and cognition retrievals are relevant and improve held-out objective performance.", + "acceptance": "Control and treatment runs on the same tasks bind retrieval receipts, relevance judgments, contamination checks, latency, token cost, and objective scores." + }, + { + "id": "prove-x-export-recovery", + "state": "open", + "priority": "P1", + "journeyIds": [ + "inspect-edit-and-recover-x-workspace", + "export-and-replay-x-run" + ], + "summary": "Prove that an X export is sufficient to recover the intended local workspace state.", + "acceptance": "A browser creates Shadow and artifact files, exports, clears the instance, restores from the export, and verifies hashes and policy boundaries." + }, + { + "id": "prove-x-replay-roundtrip", + "state": "open", + "priority": "P1", + "journeyIds": [ + "export-and-replay-x-run" + ], + "summary": "Prove the visible export and replay controls work together end to end.", + "acceptance": "A browser exports a completed run, imports that exact file, steps and plays its events, and verifies metadata, order, progress, pause, reset, and completion." + }, + { + "id": "prove-x-remote-swarm-journey", + "state": "open", + "priority": "P2", + "journeyIds": [ + "share-x-files-with-peers" + ], + "summary": "Retain evidence for a real cross-browser X peer exchange rather than inferring availability from transport tests.", + "acceptance": "Two remote deployed browsers discover through the configured signaling path, exchange a permitted file, verify its hash, and retain connection and transfer receipts." + } + ], + "constraints": [ + "X must remain a strict declared superset of Zero without silently changing Zero's tool or module surface.", + "Tool inventory proves availability, not that an operator journey works or that a model uses the capability well.", + "Promotion replay and hashes prove the declared candidate and evidence chain, not general semantic correctness.", + "A mock-model browser pass must not be presented as actual inference quality evidence.", + "Replay re-emits recorded events and must not be described as rerunning inference.", + "Worker and swarm components must not be described as a currently available distributed agent network without retained cross-browser evidence." + ] +} diff --git a/docs/status/zero-critical-user-journeys.json b/docs/status/zero-critical-user-journeys.json new file mode 100644 index 000000000..6ab124ae0 --- /dev/null +++ b/docs/status/zero-critical-user-journeys.json @@ -0,0 +1,360 @@ +{ + "schema": "reploid/zero-critical-user-journeys/v1", + "updated": "2026-07-27", + "surface": "/zero", + "productName": "Zero", + "statusDefinitions": { + "supported": "The outcome has an executable implementation and automated end-to-end contract coverage.", + "conditional": "The outcome is executable only when named model, provider, device, response-shape, or runtime prerequisites are satisfied.", + "limited": "A narrower runtime outcome works, but the complete operator journey is not yet proved.", + "blocked": "The user outcome is intentionally unavailable and has explicit blockers." + }, + "releaseEvidence": { + "gate": "scripts/verify-critical-user-journeys.js", + "retainedArtifact": null, + "status": "not-retained", + "requiredArtifactFields": [ + "journeyIds", + "commit", + "deploymentUrl", + "bootProfile", + "modelId", + "provider", + "startedAt", + "completedAt", + "cycleArtifactHashes", + "mutationArtifactHashes", + "screenshotHash", + "result" + ] + }, + "journeys": [ + { + "id": "configure-and-awaken-zero", + "actor": "operator", + "outcome": "Choose the managed Gemini proxy or a compatible local Doppler model, set the first objective and cycle interval, and awaken the CreateTool-only Zero runtime.", + "routes": [ + "/zero" + ], + "status": "conditional", + "releaseCritical": true, + "prerequisites": [ + "The selected proxy endpoint is reachable with the configured model, or the browser and device qualify for the selected Doppler artifact.", + "The objective is non-empty." + ], + "implementationPaths": [ + "self/ui/zero-home/index.js", + "self/ui/boot-home/index.js", + "self/ui/boot-wizard/state.js", + "self/host/start-app.js", + "self/config/zero-inference.js" + ], + "testPaths": [ + "tests/e2e/boot.spec.js", + "tests/e2e/boot-contract.spec.js", + "tests/unit/zero-goals.test.js" + ], + "limitations": [ + "Configuration completeness enables Awaken; a successful provider preflight is not required.", + "The ordinary browser suite does not prove current managed-endpoint availability or current local model artifact availability." + ], + "openWorkIds": [ + "retain-zero-release-evidence", + "prove-zero-inference-preflight" + ] + }, + { + "id": "run-and-observe-zero-cycle", + "actor": "operator", + "outcome": "Watch model input, model output, tool execution, runtime state, tokens, cycle count, and failures update while Zero pursues the objective.", + "routes": [ + "/zero" + ], + "status": "conditional", + "releaseCritical": true, + "prerequisites": [ + "A configured inference provider returns a response understood by the agent parser.", + "The agent loop remains inside its iteration, context, and retry limits." + ], + "implementationPaths": [ + "self/core/agent-loop.js", + "self/ui/zero/index.js", + "self/infrastructure/telemetry-timeline.js" + ], + "testPaths": [ + "tests/e2e/zero-one-safe-iteration.spec.js", + "tests/e2e/iteration-artifacts.spec.js", + "tests/unit/zero-ui.test.js" + ], + "limitations": [ + "Most deterministic cycle coverage uses a mock cognition provider.", + "The visible trace is capped at 80 entries and is not itself a retained release artifact.", + "Cycle completion proves execution and audit production, not that an arbitrary objective was solved well." + ], + "openWorkIds": [ + "retain-zero-release-evidence", + "evaluate-zero-goal-quality", + "make-zero-trace-durable" + ] + }, + { + "id": "grow-zero-tool-surface", + "actor": "agent", + "outcome": "Start with only CreateTool, activate a fixture-tested tool, replay its activation in a fresh harness, load it into the live runner, and use it in a later action.", + "routes": [ + "/zero" + ], + "status": "conditional", + "releaseCritical": true, + "prerequisites": [ + "The model emits a valid CreateTool call and complete module source.", + "The proposed tool declares deterministic activation checks that pass and replay with matching transcripts." + ], + "implementationPaths": [ + "self/tools/CreateTool.js", + "self/core/tool-runner.js", + "self/config/tool-surfaces.js" + ], + "testPaths": [ + "tests/e2e/tool-calling-smoke.spec.js", + "tests/e2e/zero-one-safe-iteration.spec.js", + "tests/e2e/zero-self-patch.spec.js", + "tests/unit/tools/create-tool.test.js" + ], + "limitations": [ + "Activation proves only the declared fixture contract and replay transcript.", + "It does not prove the tool is generally correct, secure, or useful outside those fixtures.", + "Whether a real model authors the needed tool correctly remains model-dependent." + ], + "openWorkIds": [ + "retain-zero-release-evidence", + "evaluate-created-tool-quality" + ] + }, + { + "id": "apply-zero-self-modification", + "actor": "agent", + "outcome": "Create a capability-bearing self-write tool, preserve rollback evidence, patch a live Zero tool, UI module, or mirrored core module, and activate the result through tool load, UI reload, or document reload.", + "routes": [ + "/zero" + ], + "status": "conditional", + "releaseCritical": true, + "prerequisites": [ + "CreateTool accepts and activates a tool declaring the self:write capability.", + "The generated self writer preserves rollback and mutation evidence.", + "The patch remains bootable after activation or reload." + ], + "implementationPaths": [ + "self/tools/CreateTool.js", + "self/host/start-app.js", + "self/config/vfs-policy.js" + ], + "testPaths": [ + "tests/e2e/zero-self-patch.spec.js", + "tests/e2e/zero-ui-refresh.spec.js", + "tests/unit/tools/create-tool.test.js" + ], + "limitations": [ + "Zero does not expose Promote; an activated self:write tool can write allowed live mirrors directly.", + "CreateTool replay validates declared fixtures, not the full application regression suite or a human approval.", + "The end-to-end self-patch tests drive deterministic tool calls directly rather than proving autonomous patch quality from an open-ended prompt." + ], + "openWorkIds": [ + "retain-zero-release-evidence", + "strengthen-zero-self-mutation-authority", + "evaluate-zero-goal-quality" + ] + }, + { + "id": "steer-and-stop-zero", + "actor": "operator", + "outcome": "Queue one context or correction note for the next cycle, stop an active run, cancel a pending provider retry, and manually reload the Zero UI.", + "routes": [ + "/zero" + ], + "status": "limited", + "releaseCritical": true, + "prerequisites": [ + "The Zero runtime UI is mounted.", + "A run or retry is active for the stop control to remain enabled." + ], + "implementationPaths": [ + "self/ui/zero/index.js", + "self/core/agent-loop.js" + ], + "testPaths": [ + "tests/unit/zero-ui.test.js", + "tests/e2e/zero-ui-refresh.spec.js" + ], + "limitations": [ + "Zero has no Run or Resume control after an ordinary user stop.", + "The current automated UI coverage does not exercise the complete note, stop, cancel-retry, and reload journey through user input." + ], + "openWorkIds": [ + "complete-zero-operator-control-journey" + ] + }, + { + "id": "inspect-and-recover-zero-evidence", + "actor": "operator", + "outcome": "Retain writable VFS files and cycle artifacts across reload, then inspect them after rebuilding the reader capabilities needed by the minimal surface.", + "routes": [ + "/zero" + ], + "status": "limited", + "releaseCritical": true, + "prerequisites": [ + "IndexedDB and the service worker remain available for the same Zero instance.", + "Zero has created or recreated a reader tool capable of inspecting the files." + ], + "implementationPaths": [ + "self/boot-helpers/vfs-bootstrap.js", + "self/core/vfs.js", + "self/ui/zero/index.js" + ], + "testPaths": [ + "tests/e2e/boot-replay.spec.js", + "tests/e2e/vfs-roundtrip.spec.js", + "tests/e2e/iteration-artifacts.spec.js" + ], + "limitations": [ + "The minimal Zero UI has no VFS browser, export control, or replay importer.", + "Persisted created tools are not proved to auto-load into a fresh ToolRunner after reload.", + "Recovery remains local to one browser instance." + ], + "openWorkIds": [ + "make-zero-trace-durable", + "prove-zero-tool-recovery" + ] + }, + { + "id": "prove-actual-local-rsi-cycle", + "actor": "release_reviewer", + "outcome": "Run a bounded Zero mutation cycle with actual Doppler WebGPU cognition and bind model identity, tool calls, cycle audits, and the produced artifact.", + "routes": [ + "/zero" + ], + "status": "conditional", + "releaseCritical": true, + "prerequisites": [ + "REPLOID_E2E_ACTUAL_RSI=1 is explicitly enabled.", + "Chromium WebGPU and the pinned Doppler model artifact are available.", + "The hardware-qualified run completes within its bounded test timeout." + ], + "implementationPaths": [ + "self/config/doppler-local-models.js", + "self/core/agent-loop.js", + "self/tools/CreateTool.js" + ], + "testPaths": [ + "tests/e2e/zero-actual-rsi.spec.js" + ], + "limitations": [ + "The actual-inference lane is skipped by default.", + "The prompt supplies an exact desired tool transcript, so this proves the real inference and execution boundary rather than open-ended autonomous design quality.", + "No immutable deployed artifact from the most recent run is referenced by this registry." + ], + "openWorkIds": [ + "retain-zero-release-evidence", + "evaluate-zero-goal-quality" + ] + } + ], + "openWork": [ + { + "id": "retain-zero-release-evidence", + "state": "open", + "priority": "P0", + "journeyIds": [ + "configure-and-awaken-zero", + "run-and-observe-zero-cycle", + "grow-zero-tool-surface", + "apply-zero-self-modification", + "prove-actual-local-rsi-cycle" + ], + "summary": "Persist an actual deployed Zero run as immutable journey evidence instead of relying on transient Playwright attachments or prose.", + "acceptance": "A retained artifact supplies every releaseEvidence.requiredArtifactFields value and is linked from releaseEvidence.retainedArtifact." + }, + { + "id": "evaluate-zero-goal-quality", + "state": "open", + "priority": "P0", + "journeyIds": [ + "run-and-observe-zero-cycle", + "apply-zero-self-modification", + "prove-actual-local-rsi-cycle" + ], + "summary": "Measure whether Zero solves representative open-ended objectives rather than only reproducing supplied tool transcripts.", + "acceptance": "A versioned objective set binds model, prompt, cycle receipts, final artifacts, machine checks, and human usefulness or recognizability verdicts." + }, + { + "id": "strengthen-zero-self-mutation-authority", + "state": "open", + "priority": "P0", + "journeyIds": [ + "apply-zero-self-modification" + ], + "summary": "Define and enforce the authority boundary for a dynamically created self:write tool that can bypass X's Promote lane.", + "acceptance": "The runtime enforces an explicit, tested policy for high-impact live writes, preserves rollback, and cannot self-authorize mutations to the policy or its validators." + }, + { + "id": "prove-zero-inference-preflight", + "state": "open", + "priority": "P1", + "journeyIds": [ + "configure-and-awaken-zero" + ], + "summary": "Prevent a syntactically complete but unreachable inference configuration from looking ready.", + "acceptance": "Awaken readiness distinguishes configured from verified, and tests cover failed proxy and failed local-model preflight with actionable recovery." + }, + { + "id": "complete-zero-operator-control-journey", + "state": "open", + "priority": "P1", + "journeyIds": [ + "steer-and-stop-zero" + ], + "summary": "Give Zero a complete, tested operator control loop after a stop.", + "acceptance": "Browser coverage queues a note, observes it in the next model input, stops, resumes without reloading the page, cancels a retry, and preserves correct button state." + }, + { + "id": "make-zero-trace-durable", + "state": "open", + "priority": "P1", + "journeyIds": [ + "run-and-observe-zero-cycle", + "inspect-and-recover-zero-evidence" + ], + "summary": "Make the operator-visible trace recoverable and exportable instead of retaining only a capped in-memory view.", + "acceptance": "The operator can reopen or export a cycle trace whose rows bind the corresponding VFS cycle artifacts and hashes." + }, + { + "id": "prove-zero-tool-recovery", + "state": "open", + "priority": "P1", + "journeyIds": [ + "inspect-and-recover-zero-evidence" + ], + "summary": "Prove how a tool created in one Zero session becomes available after a fresh boot.", + "acceptance": "An end-to-end test creates a tool, reloads without recreating it, safely restores and revalidates it, then executes it with an auditable activation receipt." + }, + { + "id": "evaluate-created-tool-quality", + "state": "open", + "priority": "P2", + "journeyIds": [ + "grow-zero-tool-surface" + ], + "summary": "Evaluate generated tools beyond their author-supplied activation fixtures.", + "acceptance": "Independent hidden fixtures, capability abuse checks, and held-out task checks are bound to the created tool bytes and activation evidence." + } + ], + "constraints": [ + "Zero must boot with exactly CreateTool and must not silently inherit X tools or modules.", + "Activation replay proves declared fixture behavior, not general correctness or safety.", + "A mock-model browser pass must not be presented as actual inference quality evidence.", + "A seed or cycle artifact hash proves identity and custody, not that the objective was solved.", + "Zero live self-mutation claims must disclose that it does not use X's Promote tool." + ] +} diff --git a/docs/x/critical-user-journeys.md b/docs/x/critical-user-journeys.md new file mode 100644 index 000000000..0dcd09016 --- /dev/null +++ b/docs/x/critical-user-journeys.md @@ -0,0 +1,43 @@ +# X Critical User Journeys + +X is the mature governed agent workspace at `/x`. Its canonical status registry +is [`../status/x-critical-user-journeys.json`](../status/x-critical-user-journeys.json). +X is a declared superset of Zero with prebuilt file, promotion, optimization, +cognition, worker, and swarm capabilities and a denser operator UI. + +## Current journey status + +| Journey | Status | Honest outcome | +| --- | --- | --- | +| Configure and awaken | Conditional | Browser, direct, or proxy inference can awaken the full substrate when configuration and VFS loading succeed. | +| Run, steer, stop, and resume | Conditional | Timeline, human input, stop/resume, status, and telemetry exist; the complete control loop is not yet one end-to-end test. | +| Inspect, edit, and recover workspace | Supported | The operator can browse the VFS, use writable roots, preserve state across reload, and keep direct `/self` writes blocked. | +| Evaluate and promote a candidate | Supported | Arena evaluation, byte-bound replay evidence, rollback preservation, allowlisting, quarantine, and promotion execute end to end. | +| Optimize Doppler | Conditional | The governed optimization UI and activation contracts work when current Doppler tooling and model artifacts satisfy them. | +| Delegate to workers | Limited | Worker contracts and panels exist, but the browser journey currently proves empty UI states rather than useful completed work. | +| Inspect memory and cognition | Limited | The panels expose produced records, but no held-out evaluation proves that retrieval improves outcomes. | +| Export and replay | Limited | Export and event-replay components exist, but no browser test round-trips one exported X run through the importer. | +| Share files with peers | Limited | Swarm protocols and tools are tested as components, but there is no retained deployed remote-peer exchange. | + +## Promotion boundary + +X's `Promote` copies an allowlisted Shadow candidate into `/self` only when +evidence binds the requested paths, candidate bytes, target bytes, and +`replayPassed: true`. It preserves rollback material and quarantines validator +targets. Clockwork-tagged changes require additional trusted Gamma evidence. + +Ordinary promotion is not universally human-only. `Promote` is in the X agent's +tool inventory, and the optimization UI can also invoke it. The registry tracks +making this actor and authority visible before activation. Hashes and replay +prove the declared evidence chain; they do not prove arbitrary semantic +correctness. + +## Capability versus journey + +A loaded module, registered tool, visible tab, or passing unit test is not by +itself a completed user journey. Workers, memory, replay, optimization, and +swarm are separately marked supported, conditional, or limited according to +the strongest executable user-level evidence currently present. + +Remaining work belongs only in the registry's `openWork` collection. Blueprint +checklists and architecture prose do not own current journey status. diff --git a/docs/zero/critical-user-journeys.md b/docs/zero/critical-user-journeys.md new file mode 100644 index 000000000..4ba0a519a --- /dev/null +++ b/docs/zero/critical-user-journeys.md @@ -0,0 +1,44 @@ +# Zero Critical User Journeys + +Zero is the minimal research agent at `/zero`. Its canonical status registry is +[`../status/zero-critical-user-journeys.json`](../status/zero-critical-user-journeys.json). +Zero starts with exactly `CreateTool`; it does not inherit X's file, promotion, +worker, cognition, optimization, or swarm tools. + +## Current journey status + +| Journey | Status | Honest outcome | +| --- | --- | --- | +| Configure and awaken | Conditional | The operator can choose managed proxy or local Doppler inference, set an objective and cycle interval, and awaken when configuration is complete. | +| Run and observe a cycle | Conditional | The UI shows model input/output, tool runs, state, cycles, tokens, and failures while a compatible provider responds. | +| Grow the tool surface | Conditional | CreateTool can fixture-test, replay, install, load, and use a new tool when the model authors a valid contract. | +| Apply a self-modification | Conditional | A created `self:write` tool can preserve rollback evidence and patch live tools, UI, or mirrored core code. | +| Steer and stop | Limited | The operator can queue a note and stop or cancel retry, but cannot resume from the Zero shell. | +| Inspect and recover evidence | Limited | VFS files persist locally, but Zero has no built-in VFS browser, export, replay, or automatic created-tool recovery proof. | +| Prove actual local RSI | Conditional | An opt-in hardware lane executes real Doppler cognition, but it is skipped by default and uses a supplied target transcript. | + +## Critical boundary + +CreateTool activation executes declared fixtures, re-imports the candidate in a +fresh harness, replays the fixtures, and requires matching transcripts. That is +meaningful evidence for the declared activation behavior. It is not an +independent security review, a complete regression suite, or proof that the +tool works on held-out inputs. + +Zero does not expose `Promote`. The current self-modification proof creates and +activates a capability-bearing `self:write` tool, which can write live mirrored +paths and trigger reload. This is a real behavior, not merely a proposed +architecture. The registry therefore tracks an explicit P0 authority task for +high-impact writes instead of describing Zero as if it shared X's promotion +gate. + +## Release standard + +The deterministic browser tests prove boot, tool growth, cycle artifacts, VFS +persistence, and live self-patching. Most use mock cognition or direct tool +driving. The actual Doppler test is opt-in and no immutable deployed run is +currently linked. Zero remains conditional until an artifact binds deployment, +model, provider, cycles, mutations, visible result, and final verdict. + +Remaining work belongs only in the registry's `openWork` collection. Narrative +docs and blueprint checklists do not own current journey status. diff --git a/package.json b/package.json index 8b6fa022c..114932a5d 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,11 @@ "sync:runtime-config": "node scripts/sync-runtime-config.js --write", "deploy:pool-env": "node scripts/print-pool-env.js", "deploy:simulatte-hosting": "firebase deploy --config firebase.simulatte-hosting.json --project simulatte-world --only hosting:simulatte-world", + "verify:journeys": "node scripts/verify-critical-user-journeys.js", "verify:pool": "node scripts/verify-pool-production.js", + "verify:pool:journeys": "node scripts/verify-pool-critical-user-journeys.js", + "verify:zero:journeys": "node scripts/verify-zero-critical-user-journeys.js", + "verify:x:journeys": "node scripts/verify-x-critical-user-journeys.js", "verify:pool:release": "node scripts/verify-pool-release.js", "verify:deploy-surface": "node scripts/verify-deploy-surface.js", "smoke:pool": "node scripts/pool-browser-smoke.js", diff --git a/scripts/critical-user-journey-contract.js b/scripts/critical-user-journey-contract.js new file mode 100644 index 000000000..77510f6d6 --- /dev/null +++ b/scripts/critical-user-journey-contract.js @@ -0,0 +1,169 @@ +import { promises as fs } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const SCRIPT_PATH = fileURLToPath(import.meta.url); +export const PROJECT_ROOT = path.resolve(path.dirname(SCRIPT_PATH), '..'); + +const ALLOWED_STATUSES = new Set(['supported', 'conditional', 'limited', 'blocked']); +const ALLOWED_WORK_STATES = new Set(['open', 'blocked']); + +const isNonEmptyString = (value) => typeof value === 'string' && value.trim().length > 0; +const isStringArray = (value, { empty = false } = {}) => Array.isArray(value) + && (empty || value.length > 0) + && value.every(isNonEmptyString); + +export const getJourneyRegistryPath = (surfaceName) => path.join( + PROJECT_ROOT, + 'docs', + 'status', + `${surfaceName}-critical-user-journeys.json` +); + +export const checkRepoPath = async (repoPath, label, root, errors) => { + const resolvedRoot = path.resolve(root); + const resolved = path.resolve(resolvedRoot, repoPath); + if (path.isAbsolute(repoPath) || !resolved.startsWith(`${resolvedRoot}${path.sep}`)) { + errors.push(`${label} escapes the repository: ${repoPath}`); + return; + } + try { + await fs.access(resolved); + } catch { + errors.push(`${label} is missing: ${repoPath}`); + } +}; + +export async function validateCriticalUserJourneyRegistry(registry, { + root = PROJECT_ROOT, + expectedSchema, + expectedSurface, + requiredRoutes = [] +} = {}) { + const errors = []; + if (registry?.schema !== expectedSchema) { + errors.push(`schema must be ${expectedSchema}`); + } + if (!isNonEmptyString(registry?.updated)) errors.push('updated must be a non-empty string'); + if (registry?.surface !== expectedSurface) { + errors.push(`surface must be ${expectedSurface}`); + } + if (!Array.isArray(registry?.journeys) || registry.journeys.length === 0) { + errors.push('journeys must be a non-empty array'); + return errors; + } + if (!Array.isArray(registry?.openWork)) errors.push('openWork must be an array'); + if (!isStringArray(registry?.constraints)) { + errors.push('constraints must be a non-empty string array'); + } + + const workIds = new Set(); + const journeyIds = new Set(); + const coveredRoutes = new Set(); + + for (const [position, work] of (registry.openWork || []).entries()) { + const label = `openWork[${position}]`; + if (!isNonEmptyString(work?.id)) errors.push(`${label}.id must be a non-empty string`); + else if (workIds.has(work.id)) errors.push(`${label}.id duplicates ${work.id}`); + else workIds.add(work.id); + if (!ALLOWED_WORK_STATES.has(work?.state)) errors.push(`${label}.state must be open or blocked`); + if (!/^P[0-2]$/.test(String(work?.priority || ''))) { + errors.push(`${label}.priority must be P0, P1, or P2`); + } + if (!isStringArray(work?.journeyIds)) { + errors.push(`${label}.journeyIds must be a non-empty string array`); + } + if (!isNonEmptyString(work?.summary)) errors.push(`${label}.summary must be a non-empty string`); + if (!isNonEmptyString(work?.acceptance)) { + errors.push(`${label}.acceptance must be a non-empty string`); + } + } + + for (const [position, journey] of registry.journeys.entries()) { + const label = `journeys[${position}]`; + if (!isNonEmptyString(journey?.id)) errors.push(`${label}.id must be a non-empty string`); + else if (journeyIds.has(journey.id)) errors.push(`${label}.id duplicates ${journey.id}`); + else journeyIds.add(journey.id); + if (!isNonEmptyString(journey?.actor)) errors.push(`${label}.actor must be a non-empty string`); + if (!isNonEmptyString(journey?.outcome)) errors.push(`${label}.outcome must be a non-empty string`); + if (!ALLOWED_STATUSES.has(journey?.status)) { + errors.push(`${label}.status must be supported, conditional, limited, or blocked`); + } + if (typeof journey?.releaseCritical !== 'boolean') { + errors.push(`${label}.releaseCritical must be boolean`); + } + if (!isStringArray(journey?.routes)) { + errors.push(`${label}.routes must be a non-empty string array`); + } + for (const route of journey?.routes || []) coveredRoutes.add(route); + if (!isStringArray(journey?.prerequisites, { empty: true })) { + errors.push(`${label}.prerequisites must be a string array`); + } + if (!isStringArray(journey?.limitations)) { + errors.push(`${label}.limitations must be a non-empty string array`); + } + if (!isStringArray(journey?.openWorkIds, { empty: true })) { + errors.push(`${label}.openWorkIds must be a string array`); + } + + const blocked = journey?.status === 'blocked'; + if (!isStringArray(journey?.implementationPaths, { empty: blocked })) { + errors.push( + `${label}.implementationPaths must ${blocked ? 'be a string array' : 'be a non-empty string array'}` + ); + } + if (!isStringArray(journey?.testPaths, { empty: blocked })) { + errors.push( + `${label}.testPaths must ${blocked ? 'be a string array' : 'be a non-empty string array'}` + ); + } + for (const repoPath of [...(journey?.implementationPaths || []), ...(journey?.testPaths || [])]) { + await checkRepoPath(repoPath, label, root, errors); + } + } + + for (const journey of registry.journeys) { + for (const workId of journey.openWorkIds || []) { + if (!workIds.has(workId)) { + errors.push(`journey ${journey.id} references unknown openWork ${workId}`); + } + } + } + for (const work of registry.openWork || []) { + for (const journeyId of work.journeyIds || []) { + if (!journeyIds.has(journeyId)) { + errors.push(`openWork ${work.id} references unknown journey ${journeyId}`); + } + const journey = registry.journeys.find((candidate) => candidate.id === journeyId); + if (journey && !(journey.openWorkIds || []).includes(work.id)) { + errors.push(`openWork ${work.id} is not linked back from journey ${journeyId}`); + } + } + } + for (const route of requiredRoutes) { + if (!coveredRoutes.has(route)) { + errors.push(`critical ${expectedSurface} route is not covered by a journey: ${route}`); + } + } + + if (!isNonEmptyString(registry?.releaseEvidence?.gate)) { + errors.push('releaseEvidence.gate must be a non-empty string'); + } else { + await checkRepoPath(registry.releaseEvidence.gate, 'releaseEvidence.gate', root, errors); + } + if (!isStringArray(registry?.releaseEvidence?.requiredArtifactFields)) { + errors.push('releaseEvidence.requiredArtifactFields must be a non-empty string array'); + } + if (registry?.releaseEvidence?.retainedArtifact) { + await checkRepoPath( + registry.releaseEvidence.retainedArtifact, + 'releaseEvidence.retainedArtifact', + root, + errors + ); + } else if (registry?.releaseEvidence?.status !== 'not-retained') { + errors.push('releaseEvidence without retainedArtifact must have status not-retained'); + } + + return errors; +} diff --git a/scripts/validate-registry.js b/scripts/validate-registry.js index c7a739c98..d601ce6e1 100644 --- a/scripts/validate-registry.js +++ b/scripts/validate-registry.js @@ -204,10 +204,51 @@ async function findStaleBlueprints(blueprintRegistry) { return issues; } +/** + * Find browser files declared by genesis configuration but absent from self/. + */ +async function findMissingGenesisFiles(genesisConfig, source) { + const issues = []; + const groups = []; + + for (const [category, files] of Object.entries(genesisConfig.sharedFiles || {})) { + groups.push({ scope: `sharedFiles.${category}`, files }); + } + + for (const [level, categories] of Object.entries(genesisConfig.levelFiles || {})) { + for (const [category, files] of Object.entries(categories || {})) { + groups.push({ scope: `levelFiles.${level}.${category}`, files }); + } + } + + for (const [moduleId, files] of Object.entries(genesisConfig.moduleFiles || {})) { + groups.push({ scope: `moduleFiles.${moduleId}`, files }); + } + + for (const { scope, files } of groups) { + for (const file of files || []) { + try { + await fs.access(path.join(ROOT, 'self', file)); + } catch { + issues.push({ + type: 'missing_genesis_file', + severity: 'high', + source, + scope, + file + }); + } + } + } + + return issues; +} + async function main() { console.log('[validate] Loading config files...'); - const [genesisLevels, blueprintRegistry, moduleRegistry, vfsManifest] = await Promise.all([ + const [genesisTemplate, genesisLevels, blueprintRegistry, moduleRegistry, vfsManifest] = await Promise.all([ + loadJSON('genesis-template.json'), loadJSON('genesis-levels.json'), loadJSON('blueprint-registry.json'), loadJSON('module-registry.json'), @@ -235,6 +276,10 @@ async function main() { console.log('[validate] Checking for stale blueprints...'); allIssues.push(...await findStaleBlueprints(blueprintRegistry)); + console.log('[validate] Checking genesis file inventory...'); + allIssues.push(...await findMissingGenesisFiles(genesisTemplate, 'genesis-template.json')); + allIssues.push(...await findMissingGenesisFiles(genesisLevels, 'genesis-levels.json')); + // Sort by severity allIssues.sort((a, b) => SEVERITY[a.severity] - SEVERITY[b.severity]); @@ -263,6 +308,9 @@ async function main() { case 'stale_blueprint': log(issue.type, issue.severity, `${issue.blueprint} references missing ${issue.file}`); break; + case 'missing_genesis_file': + log(issue.type, issue.severity, `${issue.source} ${issue.scope} references missing ${issue.file}`); + break; default: log(issue.type, issue.severity, JSON.stringify(issue)); } diff --git a/scripts/verify-critical-user-journeys.js b/scripts/verify-critical-user-journeys.js new file mode 100644 index 000000000..1e2a98e44 --- /dev/null +++ b/scripts/verify-critical-user-journeys.js @@ -0,0 +1,25 @@ +#!/usr/bin/env node + +import { verifyPoolCriticalUserJourneys } from './verify-pool-critical-user-journeys.js'; +import { verifyZeroCriticalUserJourneys } from './verify-zero-critical-user-journeys.js'; +import { verifyXCriticalUserJourneys } from './verify-x-critical-user-journeys.js'; + +const checks = [ + ['Poolday', verifyPoolCriticalUserJourneys], + ['Zero', verifyZeroCriticalUserJourneys], + ['X', verifyXCriticalUserJourneys] +]; + +let failed = false; +for (const [name, verify] of checks) { + const errors = await verify(); + if (errors.length === 0) { + console.log(`${name} critical user journeys verified.`); + continue; + } + failed = true; + console.error(`${name} critical user journey verification failed:`); + for (const error of errors) console.error(`- ${error}`); +} + +if (failed) process.exit(1); diff --git a/scripts/verify-pool-critical-user-journeys.js b/scripts/verify-pool-critical-user-journeys.js new file mode 100644 index 000000000..ac6f4dbc6 --- /dev/null +++ b/scripts/verify-pool-critical-user-journeys.js @@ -0,0 +1,75 @@ +#!/usr/bin/env node + +import { promises as fs } from 'node:fs'; +import path from 'node:path'; +import { pathToFileURL } from 'node:url'; +import { + PROJECT_ROOT, + getJourneyRegistryPath, + validateCriticalUserJourneyRegistry +} from './critical-user-journey-contract.js'; + +export { PROJECT_ROOT }; +export const JOURNEY_REGISTRY_PATH = getJourneyRegistryPath('poolday'); + +const REQUIRED_ROUTES = new Set(['/', '/ask', '/compute', '/records', '/history', '/network']); + +export async function validatePoolCriticalUserJourneys(registry, { + root = PROJECT_ROOT, + poolConfig = null +} = {}) { + const errors = await validateCriticalUserJourneyRegistry(registry, { + root, + expectedSchema: 'reploid/poolday-critical-user-journeys/v1', + expectedSurface: '/', + requiredRoutes: [...REQUIRED_ROUTES] + }); + + const config = poolConfig || JSON.parse( + await fs.readFile(path.join(root, 'self', 'pool', 'pool-config.json'), 'utf8') + ); + const enabledModelIds = new Set( + (config.modelCatalog || []).filter((model) => model.enabled !== false).map((model) => model.modelId) + ); + const policyIds = new Set(Object.keys(config.policies || {})); + + for (const [position, journey] of (registry.journeys || []).entries()) { + const label = `journeys[${position}]`; + for (const modelId of journey?.modelIds || []) { + if (!enabledModelIds.has(modelId)) errors.push(`${label}.modelIds is not enabled: ${modelId}`); + } + for (const policyId of journey?.policyIds || []) { + if (!policyIds.has(policyId)) { + errors.push(`${label}.policyIds is unknown: ${policyId}`); + continue; + } + for (const modelId of journey?.modelIds || []) { + if (!config.policies[policyId]?.allowedModels?.includes(modelId)) { + errors.push(`${label} model ${modelId} is not allowed by policy ${policyId}`); + } + } + } + } + + return errors; +} + +export async function verifyPoolCriticalUserJourneys(registryPath = JOURNEY_REGISTRY_PATH) { + const registry = JSON.parse(await fs.readFile(registryPath, 'utf8')); + return validatePoolCriticalUserJourneys(registry, { + root: path.resolve(path.dirname(registryPath), '..', '..') + }); +} + +const isMain = process.argv[1] + && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href; + +if (isMain) { + const errors = await verifyPoolCriticalUserJourneys(); + if (errors.length > 0) { + console.error('Poolday critical user journey verification failed:'); + for (const error of errors) console.error(`- ${error}`); + process.exit(1); + } + console.log('Poolday critical user journeys verified.'); +} diff --git a/scripts/verify-pool-production.js b/scripts/verify-pool-production.js index 76115c41f..ded4032e7 100644 --- a/scripts/verify-pool-production.js +++ b/scripts/verify-pool-production.js @@ -11,6 +11,7 @@ import { verifyModelArtifactManifest, verifyModelArtifactRangeDelivery } from '../self/pool/model-artifacts.js'; +import { verifyPoolCriticalUserJourneys } from './verify-pool-critical-user-journeys.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -321,6 +322,7 @@ const checkEnabledModelArtifacts = async () => { const reasons = [ ...checkLocalFiles(), + ...await verifyPoolCriticalUserJourneys(), ...await checkDeploymentUrl(deploymentUrl), ...await checkDirectFirestoreAccess(), ...await checkEnabledModelArtifacts() diff --git a/scripts/verify-pool-release.js b/scripts/verify-pool-release.js index 84fa1e2f0..a3094b6b1 100644 --- a/scripts/verify-pool-release.js +++ b/scripts/verify-pool-release.js @@ -59,6 +59,8 @@ const run = (label, script, scriptArgs = [], env = {}) => new Promise((resolve, }); try { + await run('critical user journey contract', 'verify-pool-critical-user-journeys.js'); + await run('deploy-surface drift gate', 'verify-deploy-surface.js', [ baseUrl, ...(isLocal ? ['--allow-local'] : []) diff --git a/scripts/verify-surface-claim-index.js b/scripts/verify-surface-claim-index.js index 572abe5c7..9620f27c3 100644 --- a/scripts/verify-surface-claim-index.js +++ b/scripts/verify-surface-claim-index.js @@ -6,6 +6,9 @@ import { promises as fs } from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; +import { validatePoolCriticalUserJourneys } from './verify-pool-critical-user-journeys.js'; +import { validateZeroCriticalUserJourneys } from './verify-zero-critical-user-journeys.js'; +import { validateXCriticalUserJourneys } from './verify-x-critical-user-journeys.js'; const SCRIPT_PATH = fileURLToPath(import.meta.url); export const PROJECT_ROOT = path.resolve(path.dirname(SCRIPT_PATH), '..'); @@ -17,14 +20,56 @@ export const SURFACE_CLAIM_INDEX_PATH = path.join( ); const ALLOWED_STATUSES = new Set(['supported', 'blocked']); +const REQUIRED_JOURNEY_REGISTRIES = Object.freeze({ + '/': 'docs/status/poolday-critical-user-journeys.json', + '/zero': 'docs/status/zero-critical-user-journeys.json', + '/x': 'docs/status/x-critical-user-journeys.json' +}); +const JOURNEY_VALIDATORS = Object.freeze({ + '/': validatePoolCriticalUserJourneys, + '/zero': validateZeroCriticalUserJourneys, + '/x': validateXCriticalUserJourneys +}); const isStringArray = (value) => Array.isArray(value) && value.every((item) => typeof item === 'string' && item.trim().length > 0); +const checkEvidencePath = async (evidencePath, label, root, errors) => { + const resolved = path.resolve(root, evidencePath); + const insideRoot = resolved.startsWith(`${path.resolve(root)}${path.sep}`); + if (path.isAbsolute(evidencePath) || !insideRoot) { + errors.push(`${label} escapes the repository: ${evidencePath}`); + return; + } + try { + await fs.access(resolved); + } catch { + errors.push(`${label} is missing: ${evidencePath}`); + } +}; + export async function validateSurfaceClaimIndex(index, { root = PROJECT_ROOT } = {}) { const errors = []; - if (index?.schema !== 'reploid/surface-claim-index/v1') { - errors.push('schema must be reploid/surface-claim-index/v1'); + if (index?.schema !== 'reploid/surface-claim-index/v2') { + errors.push('schema must be reploid/surface-claim-index/v2'); + } + if (!index?.journeyRegistries || typeof index.journeyRegistries !== 'object') { + errors.push('journeyRegistries must map every product route to its registry'); + } else { + for (const [surface, registryPath] of Object.entries(REQUIRED_JOURNEY_REGISTRIES)) { + if (index.journeyRegistries[surface] !== registryPath) { + errors.push(`journeyRegistries.${surface} must be ${registryPath}`); + continue; + } + await checkEvidencePath(registryPath, `journeyRegistries.${surface}`, root, errors); + try { + const registry = JSON.parse(await fs.readFile(path.resolve(root, registryPath), 'utf8')); + const journeyErrors = await JOURNEY_VALIDATORS[surface](registry, { root }); + errors.push(...journeyErrors.map((error) => `journeyRegistries.${surface}: ${error}`)); + } catch (error) { + errors.push(`journeyRegistries.${surface} could not be validated: ${error.message}`); + } + } } if (!Array.isArray(index?.entries) || index.entries.length === 0) { errors.push('entries must be a non-empty array'); @@ -65,17 +110,12 @@ export async function validateSurfaceClaimIndex(index, { root = PROJECT_ROOT } = } for (const evidencePath of entry?.evidencePaths || []) { - const resolved = path.resolve(root, evidencePath); - const insideRoot = resolved.startsWith(`${path.resolve(root)}${path.sep}`); - if (path.isAbsolute(evidencePath) || !insideRoot) { - errors.push(`${label}.evidencePaths escapes the repository: ${evidencePath}`); - continue; - } - try { - await fs.access(resolved); - } catch { - errors.push(`${label}.evidencePaths is missing: ${evidencePath}`); - } + await checkEvidencePath(evidencePath, `${label}.evidencePaths`, root, errors); + } + + const journeyRegistry = REQUIRED_JOURNEY_REGISTRIES[entry?.surface]; + if (journeyRegistry && !entry?.evidencePaths?.includes(journeyRegistry)) { + errors.push(`${label}.evidencePaths must include ${journeyRegistry}`); } } diff --git a/scripts/verify-x-critical-user-journeys.js b/scripts/verify-x-critical-user-journeys.js new file mode 100644 index 000000000..3c28847e6 --- /dev/null +++ b/scripts/verify-x-critical-user-journeys.js @@ -0,0 +1,42 @@ +#!/usr/bin/env node + +import { promises as fs } from 'node:fs'; +import path from 'node:path'; +import { pathToFileURL } from 'node:url'; +import { + PROJECT_ROOT, + getJourneyRegistryPath, + validateCriticalUserJourneyRegistry +} from './critical-user-journey-contract.js'; + +export { PROJECT_ROOT }; +export const JOURNEY_REGISTRY_PATH = getJourneyRegistryPath('x'); + +export const validateXCriticalUserJourneys = (registry, { root = PROJECT_ROOT } = {}) => ( + validateCriticalUserJourneyRegistry(registry, { + root, + expectedSchema: 'reploid/x-critical-user-journeys/v1', + expectedSurface: '/x', + requiredRoutes: ['/x'] + }) +); + +export async function verifyXCriticalUserJourneys(registryPath = JOURNEY_REGISTRY_PATH) { + const registry = JSON.parse(await fs.readFile(registryPath, 'utf8')); + return validateXCriticalUserJourneys(registry, { + root: path.resolve(path.dirname(registryPath), '..', '..') + }); +} + +const isMain = process.argv[1] + && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href; + +if (isMain) { + const errors = await verifyXCriticalUserJourneys(); + if (errors.length > 0) { + console.error('X critical user journey verification failed:'); + for (const error of errors) console.error(`- ${error}`); + process.exit(1); + } + console.log('X critical user journeys verified.'); +} diff --git a/scripts/verify-zero-critical-user-journeys.js b/scripts/verify-zero-critical-user-journeys.js new file mode 100644 index 000000000..73313cda9 --- /dev/null +++ b/scripts/verify-zero-critical-user-journeys.js @@ -0,0 +1,42 @@ +#!/usr/bin/env node + +import { promises as fs } from 'node:fs'; +import path from 'node:path'; +import { pathToFileURL } from 'node:url'; +import { + PROJECT_ROOT, + getJourneyRegistryPath, + validateCriticalUserJourneyRegistry +} from './critical-user-journey-contract.js'; + +export { PROJECT_ROOT }; +export const JOURNEY_REGISTRY_PATH = getJourneyRegistryPath('zero'); + +export const validateZeroCriticalUserJourneys = (registry, { root = PROJECT_ROOT } = {}) => ( + validateCriticalUserJourneyRegistry(registry, { + root, + expectedSchema: 'reploid/zero-critical-user-journeys/v1', + expectedSurface: '/zero', + requiredRoutes: ['/zero'] + }) +); + +export async function verifyZeroCriticalUserJourneys(registryPath = JOURNEY_REGISTRY_PATH) { + const registry = JSON.parse(await fs.readFile(registryPath, 'utf8')); + return validateZeroCriticalUserJourneys(registry, { + root: path.resolve(path.dirname(registryPath), '..', '..') + }); +} + +const isMain = process.argv[1] + && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href; + +if (isMain) { + const errors = await verifyZeroCriticalUserJourneys(); + if (errors.length > 0) { + console.error('Zero critical user journey verification failed:'); + for (const error of errors) console.error(`- ${error}`); + process.exit(1); + } + console.log('Zero critical user journeys verified.'); +} diff --git a/self/blueprints/0x000068-hierarchical-memory-architecture.md b/self/blueprints/0x000068-hierarchical-memory-architecture.md index 6b0489f27..24e0608e4 100644 --- a/self/blueprints/0x000068-hierarchical-memory-architecture.md +++ b/self/blueprints/0x000068-hierarchical-memory-architecture.md @@ -10,7 +10,7 @@ **Category:** State & Memory -**Phase:** 4 (Current - see TODO.md) +**Phase:** 4 research design --- diff --git a/self/blueprints/0x00012e-pool-policy-router.md b/self/blueprints/0x00012e-pool-policy-router.md index 9ba60b3c9..8c050e7b0 100644 --- a/self/blueprints/0x00012e-pool-policy-router.md +++ b/self/blueprints/0x00012e-pool-policy-router.md @@ -1,15 +1,15 @@ # Blueprint 0x00012e: pool policy router -**Objective:** Describe implementation for pool/policy-router.js. +**Objective:** Define shared Poolday policy validation and runtime-specific request routing. **Target Upgrade:** pool/policy-router.js -**Affected Artifacts:** /pool/policy-router.js +**Affected Artifacts:** /pool/policy-router.js, /pool/policy-validation.js --- ### 1. Intent -Define the purpose and constraints for pool/policy-router.js. +Keep policy classification and deterministic-generation validation identical across browser and server request paths. ### 2. Architecture Outline the main responsibilities, dependencies, and data flow. diff --git a/self/blueprints/implementation-status.md b/self/blueprints/implementation-status.md deleted file mode 100644 index 002a8c163..000000000 --- a/self/blueprints/implementation-status.md +++ /dev/null @@ -1,212 +0,0 @@ -# Blueprint Implementation Status - -Generated: December 2025 - ---- - -## Category 1: IMPLEMENTED (Blueprint + Code Exists) - -| Blueprint | Implementation File(s) | -|-----------|------------------------| -| `0x000001` System Prompt Architecture | `core/agent-loop.js` | -| `0x000002` Application Orchestration | `entry/start-app.js`, `sw-module-loader.js` | -| `0x000003` Core Utilities & Error Handling | `core/utils.js` | -| `0x000005` State Management Architecture | `core/state-manager.js` | -| `0x000006` Pure State Helpers | `core/state-helpers-pure.js` | -| `0x000007` API Client & Communication | `core/llm-client.js` | -| `0x000008` Agent Cognitive Cycle | `core/agent-loop.js` | -| `0x000009` Pure Agent Logic Helpers | `core/agent-loop.js` (inline) | -| `0x00000A` Tool Runner Engine | `core/tool-runner.js` | -| `0x00000B` Pure Tool Logic Helpers | `core/tool-runner.js` (inline) | -| `0x00000C` Sandboxed Tool Worker | `core/worker-agent.js` | -| `0x00000D` UI Manager | `ui/dashboard/ui-manager.js` | -| `0x00000E` UI Styling (CSS) | `ui/styles/` | -| `0x00000F` UI Body Template (HTML) | `index.html` | -| `0x000010` Static Tool Manifest | `tools/*.js` (30+ files) | -| `0x000011` Advanced Storage (IndexedDB) | `core/vfs.js`, `infrastructure/indexed-db-helper.js` | -| `0x000013` System Configuration | `config/` | -| `0x000015` Dynamic Tool Creation | `core/tool-writer.js`, `tools/CreateTool.js` | -| `0x00001C` Write Tools Manifest | `tools/WriteFile.js`, `tools/EditFile.js`, etc. | -| `0x000021` Multi-Provider API Gateway | `core/llm-client.js` | -| `0x000022` Confirmation Modal Safety | `ui/components/confirmation-modal.js` | -| `0x000023` VFS Explorer Interaction | `ui/dashboard/vfs-explorer.js` | -| `0x00002B` Toast Notification System | `ui/components/toast-notifications.js`, `ui/toast.js` | -| `0x00002C` Rate Limiting Strategies | `infrastructure/rate-limiter.js` | -| `0x00002E` Audit Logging Policy | `infrastructure/audit-logger.js` | -| `0x000030` Pyodide Runtime Orchestration | `tools/python/pyodide-runtime.js` | -| `0x000031` Python Tool Interface | `tools/python/python-tool.js` | -| `0x000032` Local LLM Runtime | `core/transformers-client.js` | -| `0x000033` Hybrid LLM Orchestration | `capabilities/intelligence/multi-model-coordinator.js` | -| `0x000034` Swarm Orchestration | `capabilities/communication/swarm-sync.js` | -| `0x000035` Reflection Store Architecture | `capabilities/reflection/reflection-store.js` | -| `0x00003C` Self-Testing Framework | `capabilities/testing/self-tester.js` | -| `0x00003D` Browser API Integration | `infrastructure/browser-apis.js` | -| `0x00003E` WebRTC Swarm Transport | `capabilities/communication/webrtc-swarm.js` | -| `0x00003F` Streaming Response Handler | `infrastructure/stream-parser.js` | -| `0x000040` Context Management | `core/context-manager.js` | -| `0x000043` Genesis Snapshot System | `infrastructure/genesis-snapshot.js` | -| `0x000046` Diff Utilities | `ui/components/diff-viewer-ui.js` | -| `0x000047` Verification Manager | `core/verification-manager.js`, `core/verification-worker.js` | -| `0x000048` Module Widget Protocol | `ui/proto/` | -| `0x000049` Dependency Injection Container | `infrastructure/di-container.js` | -| `0x00004B` Persona Management | `core/persona-manager.js` | -| `0x00004C` HITL Control Panel UI | `ui/components/hitl-widget.js` | -| `0x00004F` Worker Pool Parallelization | `core/worker-manager.js` | -| `0x000050` Diff Viewer UI | `ui/components/diff-viewer-ui.js` | -| `0x000051` HITL Controller | `infrastructure/hitl-controller.js` | -| `0x000052` Hot Module Reload | `infrastructure/vfs-hmr.js` | -| `0x000054` Module Proto Orchestration | `ui/proto/index.js` | -| `0x000058` Event Bus Infrastructure | `infrastructure/event-bus.js` | -| `0x000067` Circuit Breaker Pattern | `infrastructure/circuit-breaker.js` | -| `0x000068` Transformers.js Client | `core/transformers-client.js` | -| `0x000069` Embedding Store | `capabilities/cognition/semantic/embedding-store.js` | -| `0x000070` Semantic Memory | `capabilities/cognition/semantic/semantic-memory.js` | -| `0x000071` Knowledge Graph | `capabilities/cognition/symbolic/knowledge-graph.js` | -| `0x000072` Rule Engine | `capabilities/cognition/symbolic/rule-engine.js` | -| `0x000073` Symbol Grounder | `capabilities/cognition/symbolic/symbol-grounder.js` | -| `0x000074` Cognition API | `capabilities/cognition/cognition-api.js` | -| `0x000075` Arena Competitor | `testing/arena/competitor.js` | -| `0x000076` Arena Metrics | `testing/arena/arena-metrics.js` | -| `0x000077` Arena Harness | `testing/arena/arena-harness.js` | -| `0x000026` Performance Monitoring Stack | `capabilities/performance/performance-monitor.js` | -| `0x000027` Metrics Proto Visuals | `ui/panels/metrics-panel.js`, `ui/dashboard/metrics-dashboard.js` | -| `0x00005B` Goal Panel | `ui/goal-history.js` | - -**Count: 61 blueprints implemented** - ---- - -## Category 2: NOT IMPLEMENTED (Blueprint Exists, No Code) - -| Blueprint | Description | -|-----------|-------------| -| `0x000004` | Default Storage (localStorage) - superseded by IndexedDB | -| `0x000012` | Structured Self-Evaluation | -| `0x000014` | Working Memory Scratchpad | -| `0x000017` | Goal Modification Safety | -| `0x000018` | Blueprint Creation Meta | -| `0x000019` | Visual Self-Improvement | -| `0x00001A` | RFC Authoring | -| `0x00001B` | Code Introspection | -| `0x00001D` | Autonomous Curator Mode | -| `0x00001E` | Penteract Analytics | -| `0x000024` | Canvas Visualization Engine | -| `0x000025` | Visualization Data Adapter | -| `0x000028` | Agent FSM Visualizer | -| `0x000029` | AST Visualization Framework | -| `0x00002A` | Module Graph Visualizer | -| `0x00002D` | Module Integrity Verification | -| `0x00002F` | Interactive Tutorial System | -| `0x000038` | Tool Usage Analytics | -| `0x000039` | API Cost Tracker | -| `0x00003A` | Tab Coordination | -| `0x00003B` | Tool Documentation Generator | -| `0x000042` | DOGS/CATS Browser Parser | -| `0x000044` | Déjà Vu Pattern Detection | -| `0x000045` | Meta-Cognitive Coordination | -| `0x00004D` | Sentinel Tools Library | -| `0x00004E` | Tool Execution Panel | -| `0x000053` | git VFS Version Control | -| `0x000055` | Pyodide Worker Visualization | -| `0x000057` | Penteract Visualizer | -| `0x000059` | Sentinel FSM | -| `0x00005A` | Thought Panel | -| `0x00005E` | Sentinel Panel | -| `0x00005F` | Progress Tracker | -| `0x000060` | Status Bar | -| `0x000061` | Log Panel | -| `0x000062` | Internal Patch Format | -| `0x000063` | Browser Native Paxos | -| `0x000064` | Recursive Prompt Engineering | -| `0x000065` | Meta-Cognitive Evaluator | -| `0x000066` | Recursive Goal Decomposition | -| `0x000078` | GEPA Prompt Evolution | -| `0x000079` | Hierarchical Memory Architecture | -| `0x000080` | App Mounting System | - -**Count: 43 blueprints not yet implemented** - ---- - -## Category 3: MISSING BLUEPRINT (Code Exists, No Blueprint) - -| Implementation File | Description | Suggested Blueprint | -|---------------------|-------------|---------------------| -| `core/response-parser.js` | Parses LLM responses, extracts tool calls | 0x000081 Response Parser | -| `core/schema-registry.js` | Manages JSON schemas for tools | 0x000082 Schema Registry | -| `infrastructure/error-store.js` | Stores and retrieves errors | 0x000083 Error Store | -| `infrastructure/replay-engine.js` | Replays agent sessions | 0x000085 Replay Engine | -| `infrastructure/telemetry-timeline.js` | Timeline of telemetry events | 0x000086 Telemetry Timeline | -| `infrastructure/tool-executor.js` | Low-level tool execution | (merge into 0x00000A?) | -| `capabilities/cognition/index.js` | Cognition module entry | (part of 0x000074) | -| `capabilities/communication/swarm-transport.js` | Transport layer for swarm | (part of 0x00003E?) | -| `capabilities/reflection/reflection-analyzer.js` | Analyzes reflections | (merge into 0x000035) | -| `capabilities/system/substrate-loader.js` | Loads substrate modules | 0x000088 Substrate Loader | -| `server/agent-bridge.js` | Server-side agent bridge | 0x000089 Agent Bridge Server | -| `server/proxy.js` | Proxy server | 0x00008A Proxy Server | -| `server/signaling-server.js` | WebRTC signaling | (part of 0x00003E?) | -| `testing/arena/vfs-sandbox.js` | VFS sandbox for arena | (part of 0x000075) | -| `testing/arena/index.js` | Arena module entry | (part of 0x000077) | -| `tools/python/pyodide-worker.js` | Pyodide web worker | (part of 0x000030) | -| `ui/boot-wizard/model-config/*.js` | Model configuration UI (5 files) | 0x00008B Model Config UI | -| `ui/components/inline-chat.js` | Inline chat component | 0x00008C Inline Chat | -| `ui/panels/chat-panel.js` | Chat panel | 0x00008D Chat Panel | -| `ui/panels/code-panel.js` | Code editor panel | 0x00008E Code Panel | -| `ui/panels/cognition-panel.js` | Cognition/thought panel | (implements 0x00005A?) | -| `ui/panels/llm-config-panel.js` | LLM config panel | 0x00008F LLM Config Panel | -| `ui/panels/python-repl-panel.js` | Python REPL panel | 0x000090 Python REPL Panel | -| `ui/panels/vfs-panel.js` | VFS panel | (part of 0x000023?) | -| `ui/proto/replay.js` | Replay functionality | (part of 0x000085?) | -| `ui/proto/schemas.js` | Proto schemas | (part of 0x000048) | -| `ui/proto/telemetry.js` | Proto telemetry | (part of 0x000086?) | -| `ui/proto/template.js` | Proto templates | (part of 0x000048) | -| `ui/proto/utils.js` | Proto utilities | (part of 0x000048) | -| `ui/proto/vfs.js` | Proto VFS integration | (part of 0x000048) | -| `ui/proto/workers.js` | Proto workers integration | (part of 0x000048) | - -**Count: ~20 implementations needing blueprints (after deduplication)** - ---- - -## Summary - -| Category | Count | -|----------|-------| -| 1. IMPLEMENTED (Blueprint + Code) | 61 | -| 2. NOT IMPLEMENTED (Blueprint only) | 43 | -| 3. MISSING BLUEPRINT (Code only) | ~15 | -| **Total Blueprints** | 106 | -| **Total Implementations** | 127 | - ---- - -## Recommendations - -### New Blueprints to Create (Category 3) -1. `0x000081` Response Parser - `core/response-parser.js` -2. `0x000082` Schema Registry - `core/schema-registry.js` -3. `0x000083` Error Store - `infrastructure/error-store.js` -4. `0x000085` Replay Engine - `infrastructure/replay-engine.js` -5. `0x000086` Telemetry Timeline - `infrastructure/telemetry-timeline.js` -6. `0x000087` Substrate Loader - `capabilities/system/substrate-loader.js` -7. `0x000088` Agent Bridge Server - `server/agent-bridge.js` -8. `0x000089` Proxy Server - `server/proxy.js` -9. `0x00008A` Model Config UI - `ui/boot-wizard/model-config/*.js` -10. `0x00008B` Inline Chat - `ui/components/inline-chat.js` -11. `0x00008C` Chat Panel - `ui/panels/chat-panel.js` -12. `0x00008D` Code Panel - `ui/panels/code-panel.js` -13. `0x00008E` LLM Config Panel - `ui/panels/llm-config-panel.js` -14. `0x00008F` Python REPL Panel - `ui/panels/python-repl-panel.js` - -### Verified Matches (moved to Category 1) -- `0x000026` Performance Monitoring ← `performance-monitor.js` ✓ -- `0x000027` Metrics Proto Visuals ← `metrics-panel.js` ✓ -- `0x00005B` Goal Panel ← `goal-history.js` ✓ - -### Still Needs Verification -- `0x00005A` Thought Panel - `cognition-panel.js` is different (knowledge graph viz) - -### Files to Merge Into Existing Blueprints -- `reflection-analyzer.js` → merge into `0x000035` -- `swarm-transport.js` → merge into `0x00003E` -- `tool-executor.js` → merge into `0x00000A` diff --git a/self/boot-helpers/config.js b/self/boot-helpers/config.js index 79558cc00..f424134fd 100644 --- a/self/boot-helpers/config.js +++ b/self/boot-helpers/config.js @@ -5,7 +5,7 @@ import { applyModuleOverrides, normalizeOverrides, resolveBaseModules } from '../config/module-resolution.js'; import { getDefaultGenesisLevelForMode, normalizeBootMode } from '../config/boot-modes.js'; -import { getLabRouteProfileByPath } from '../config/lab-route-profiles.js'; +import { getLabRouteProfileByPath } from '../lab/profiles.js'; import { readVfsFile } from './vfs-bootstrap.js'; const readJsonFromVfs = async (path) => { diff --git a/self/boot-helpers/vfs-hydrate.js b/self/boot-helpers/vfs-hydrate.js index eac99fce7..1fa29de46 100644 --- a/self/boot-helpers/vfs-hydrate.js +++ b/self/boot-helpers/vfs-hydrate.js @@ -81,12 +81,10 @@ export async function resetSession(vfs, genesisConfig, genesisLevel, logger) { // Use preserveOnReset from config, or fall back to defaults const preserveConfig = genesisConfig.preserveOnReset || {}; const coreTools = buildCoreToolSet(genesisConfig, genesisLevel); - const coreUIFiles = new Set(preserveConfig.ui || ['proto.js', 'toast.js']); + const coreUIFiles = new Set(preserveConfig.ui || ['toast.js']); const coreStyles = new Set(preserveConfig.styles || [ 'zero.css', 'rd.css', - 'landing-mono.css', - 'vfs-explorer.css', 'index.css', 'layout.css', 'components.css', diff --git a/self/boot-spec.js b/self/boot-spec.js index b08636711..d914618e3 100644 --- a/self/boot-spec.js +++ b/self/boot-spec.js @@ -2,7 +2,7 @@ * @fileoverview Strict boot contract for the self-owned runtime, host, and kernel. */ -import { LAB_ROUTE_BOOT_SPECS } from './config/lab-route-profiles.js'; +import { LAB_ROUTE_BOOT_SPECS } from './lab/profiles.js'; import { OPFS_ARTIFACT_ROOTS, WRITABLE_VFS_ROOTS } from './config/vfs-policy.js'; const clone = (value) => JSON.parse(JSON.stringify(value)); diff --git a/self/capabilities/README.md b/self/capabilities/README.md index f4171efc4..7f10488c1 100644 --- a/self/capabilities/README.md +++ b/self/capabilities/README.md @@ -63,10 +63,6 @@ This directory contains advanced capabilities organized by domain. These are NOT ### intelligence/ | Module | File | Description | |--------|------|-------------| -| MultiModelCoordinator | `intelligence/multi-model-coordinator.js` | Multi-model orchestration (shim to experimental) | -| MultiModelEvaluator | `intelligence/multi-model-evaluator.js` | Multi-model evaluation harness (shim to core) | -| FunctionGemmaOrchestrator | `intelligence/functiongemma-orchestrator.js` | Doppler multi-model execution and topology evolution | -| NeuralCompiler | `intelligence/neural-compiler.js` | LoRA adapter routing and batching (shim to experimental) | | IntentBundleLoRA | `intelligence/intent-bundle-lora.js` | Intent bundle gate for LoRA adapters | ## Related diff --git a/self/capabilities/cognition/prompt-memory.js b/self/capabilities/cognition/prompt-memory.js index 9b66dde3f..158616782 100644 --- a/self/capabilities/cognition/prompt-memory.js +++ b/self/capabilities/cognition/prompt-memory.js @@ -2,8 +2,6 @@ * @fileoverview Prompt Memory * Integration layer between GEPA and SemanticMemory. * Stores evolved prompts, enables transfer learning, tracks performance drift. - * - * @see TODO.md: Memory + GEPA Integration (Phase 3) */ const PromptMemory = { diff --git a/self/config/blueprint-registry.json b/self/config/blueprint-registry.json index 9a6d64277..b12b21237 100644 --- a/self/config/blueprint-registry.json +++ b/self/config/blueprint-registry.json @@ -1,6 +1,6 @@ { "version": 1, - "generatedAt": "2026-07-26T22:19:21.348Z", + "generatedAt": "2026-07-27T15:48:28.191Z", "features": [ { "id": "0x000003", @@ -86,20 +86,6 @@ "core/tool-runner.js" ] }, - { - "id": "0x00000D", - "name": "ui-manager", - "status": "active", - "blueprints": [ - { - "id": "0x00000D", - "path": "blueprints/0x00000D-ui-manager.md" - } - ], - "files": [ - "ui/dashboard/ui-manager.js" - ] - }, { "id": "0x000011", "name": "advanced-storage-backend-indexeddb", @@ -129,20 +115,6 @@ "tools/CreateTool.js" ] }, - { - "id": "0x000020", - "name": "vfs-explorer-interaction", - "status": "active", - "blueprints": [ - { - "id": "0x000020", - "path": "blueprints/0x000020-vfs-explorer-interaction.md" - } - ], - "files": [ - "ui/dashboard/vfs-explorer.js" - ] - }, { "id": "0x000023", "name": "performance-monitoring-stack", @@ -157,20 +129,6 @@ "capabilities/performance/performance-monitor.js" ] }, - { - "id": "0x000024", - "name": "metrics-proto-visuals", - "status": "active", - "blueprints": [ - { - "id": "0x000024", - "path": "blueprints/0x000024-metrics-proto-visuals.md" - } - ], - "files": [ - "ui/dashboard/metrics-dashboard.js" - ] - }, { "id": "0x000029", "name": "rate-limiting-strategies", @@ -341,20 +299,6 @@ "core/persona-manager.js" ] }, - { - "id": "0x000044", - "name": "hitl-control-panel-ui", - "status": "active", - "blueprints": [ - { - "id": "0x000044", - "path": "blueprints/0x000044-hitl-control-panel-ui.md" - } - ], - "files": [ - "ui/components/hitl-widget.js" - ] - }, { "id": "0x000047", "name": "worker-pool-parallelization", @@ -383,20 +327,6 @@ "infrastructure/hitl-controller.js" ] }, - { - "id": "0x00004C", - "name": "module-proto-orchestration", - "status": "active", - "blueprints": [ - { - "id": "0x00004C", - "path": "blueprints/0x00004C-module-proto-orchestration.md" - } - ], - "files": [ - "ui/proto.js" - ] - }, { "id": "0x00004F", "name": "event-bus-infrastructure", @@ -750,34 +680,6 @@ "ui/panels/cognition-panel.js" ] }, - { - "id": "0x00007C", - "name": "toast-notifications", - "status": "active", - "blueprints": [ - { - "id": "0x00007C", - "path": "blueprints/0x00007C-toast-notifications.md" - } - ], - "files": [ - "ui/components/toast-notifications.js" - ] - }, - { - "id": "0x00007D", - "name": "diff-viewer-ui", - "status": "active", - "blueprints": [ - { - "id": "0x00007D", - "path": "blueprints/0x00007D-diff-viewer-ui.md" - } - ], - "files": [ - "ui/components/diff-viewer-ui.js" - ] - }, { "id": "0x000090", "name": "boot", @@ -974,20 +876,6 @@ "capabilities/reflection/prompt-score-map.js" ] }, - { - "id": "0x0000a2", - "name": "core-async-utils", - "status": "active", - "blueprints": [ - { - "id": "0x0000a2", - "path": "blueprints/0x0000a2-core-async-utils.md" - } - ], - "files": [ - "core/async-utils.js" - ] - }, { "id": "0x0000a3", "name": "core-schema-validator", @@ -1058,34 +946,6 @@ "sw-module-loader.js" ] }, - { - "id": "0x0000a8", - "name": "testing-arena-doppler-integration", - "status": "active", - "blueprints": [ - { - "id": "0x0000a8", - "path": "blueprints/0x0000a8-testing-arena-doppler-integration.md" - } - ], - "files": [ - "testing/arena/doppler-integration.js" - ] - }, - { - "id": "0x0000a9", - "name": "testing-arena-index", - "status": "active", - "blueprints": [ - { - "id": "0x0000a9", - "path": "blueprints/0x0000a9-testing-arena-index.md" - } - ], - "files": [ - "testing/arena/index.js" - ] - }, { "id": "0x0000aa", "name": "tools-awaitworkers", @@ -1548,20 +1408,6 @@ "ui/boot-wizard/steps/choose.js" ] }, - { - "id": "0x0000ce", - "name": "ui-boot-steps-detect", - "status": "active", - "blueprints": [ - { - "id": "0x0000ce", - "path": "blueprints/0x0000ce-ui-boot-steps-detect.md" - } - ], - "files": [ - "ui/boot-wizard/steps/detect.js" - ] - }, { "id": "0x0000cf", "name": "ui-boot-steps-direct", @@ -1604,48 +1450,6 @@ "ui/boot-wizard/steps/proxy.js" ] }, - { - "id": "0x0000d2", - "name": "ui-components-arena-results", - "status": "active", - "blueprints": [ - { - "id": "0x0000d2", - "path": "blueprints/0x0000d2-ui-components-arena-results.md" - } - ], - "files": [ - "ui/components/arena-results.js" - ] - }, - { - "id": "0x0000d3", - "name": "ui-components-confirmation-modal", - "status": "active", - "blueprints": [ - { - "id": "0x0000d3", - "path": "blueprints/0x0000d3-ui-components-confirmation-modal.md" - } - ], - "files": [ - "ui/components/confirmation-modal.js" - ] - }, - { - "id": "0x0000d4", - "name": "ui-panels-metrics-panel", - "status": "active", - "blueprints": [ - { - "id": "0x0000d4", - "path": "blueprints/0x0000d4-ui-panels-metrics-panel.md" - } - ], - "files": [ - "ui/panels/metrics-panel.js" - ] - }, { "id": "0x0000d5", "name": "ui-proto-index", @@ -1660,20 +1464,6 @@ "ui/proto/index.js" ] }, - { - "id": "0x0000d6", - "name": "ui-proto-schemas", - "status": "active", - "blueprints": [ - { - "id": "0x0000d6", - "path": "blueprints/0x0000d6-ui-proto-schemas.md" - } - ], - "files": [ - "ui/proto/schemas.js" - ] - }, { "id": "0x0000d7", "name": "ui-proto-telemetry", @@ -2388,20 +2178,6 @@ "sw.js" ] }, - { - "id": "0x00010c", - "name": "ui-ui", - "status": "active", - "blueprints": [ - { - "id": "0x00010c", - "path": "blueprints/0x00010c-ui-ui.md" - } - ], - "files": [ - "ui/UI.js" - ] - }, { "id": "0x00010d", "name": "ui-boot-home-index", @@ -2444,20 +2220,6 @@ "ui/boot-wizard/self-preview.js" ] }, - { - "id": "0x000110", - "name": "ui-capsule-index", - "status": "active", - "blueprints": [ - { - "id": "0x000110", - "path": "blueprints/0x000110-ui-capsule-index.md" - } - ], - "files": [ - "ui/capsule/index.js" - ] - }, { "id": "0x000111", "name": "ui-zero-index", @@ -2472,20 +2234,6 @@ "ui/zero/index.js" ] }, - { - "id": "0x000113", - "name": "config-lab-route-profiles", - "status": "active", - "blueprints": [ - { - "id": "0x000113", - "path": "blueprints/0x000113-config-lab-route-profiles.md" - } - ], - "files": [ - "config/lab-route-profiles.js" - ] - }, { "id": "0x000114", "name": "config-tool-surfaces", @@ -2696,20 +2444,6 @@ "pool/inference-receipt.js" ] }, - { - "id": "0x000123", - "name": "pool-layer-scheduler", - "status": "active", - "blueprints": [ - { - "id": "0x000123", - "path": "blueprints/0x000123-pool-layer-scheduler.md" - } - ], - "files": [ - "pool/layer-scheduler.js" - ] - }, { "id": "0x000124", "name": "pool-model-artifacts", @@ -2794,20 +2528,6 @@ "pool/peer-control-plane.js" ] }, - { - "id": "0x00012a", - "name": "pool-peer-registry", - "status": "active", - "blueprints": [ - { - "id": "0x00012a", - "path": "blueprints/0x00012a-pool-peer-registry.md" - } - ], - "files": [ - "pool/peer-registry.js" - ] - }, { "id": "0x00012b", "name": "pool-peer-rendezvous", @@ -2861,7 +2581,8 @@ } ], "files": [ - "pool/policy-router.js" + "pool/policy-router.js", + "pool/policy-validation.js" ] }, { @@ -2934,20 +2655,6 @@ "pool/sdk.js" ] }, - { - "id": "0x000134", - "name": "pool-shard-negotiation", - "status": "active", - "blueprints": [ - { - "id": "0x000134", - "path": "blueprints/0x000134-pool-shard-negotiation.md" - } - ], - "files": [ - "pool/shard-negotiation.js" - ] - }, { "id": "0x000135", "name": "self-dream-instance", @@ -2990,20 +2697,6 @@ "tools/Promote.js" ] }, - { - "id": "0x000138", - "name": "ui-boot-wizard-zero-function", - "status": "active", - "blueprints": [ - { - "id": "0x000138", - "path": "blueprints/0x000138-ui-boot-wizard-zero-function.md" - } - ], - "files": [ - "ui/boot-wizard/zero-function.js" - ] - }, { "id": "0x000139", "name": "ui-pool-home-constants", diff --git a/self/config/boot-seed.js b/self/config/boot-seed.js index d8dc868ca..50385eb48 100644 --- a/self/config/boot-seed.js +++ b/self/config/boot-seed.js @@ -134,7 +134,6 @@ export const ZERO_HOME_BOOT_SEED_PREFIXES = Object.freeze([ 'config/doppler-local-models.js', 'config/genesis-levels.json', 'config/immutability.js', - 'config/lab-route-profiles.js', 'config/module-registry.json', 'config/module-resolution.js', 'config/reploid-environments.js', diff --git a/self/config/genesis-levels.json b/self/config/genesis-levels.json index 8bc84757c..25db96a5f 100644 --- a/self/config/genesis-levels.json +++ b/self/config/genesis-levels.json @@ -344,40 +344,23 @@ "tools/git.js" ], "ui": [ - "ui/proto.js", "ui/proto/index.js", "ui/proto/optimization.js", "ui/proto/template.js", "ui/proto/telemetry.js", - "ui/proto/schemas.js", "ui/proto/workers.js", "ui/proto/vfs.js", "ui/proto/utils.js", "ui/toast.js", - "ui/panels/chat-panel.js", - "ui/panels/code-panel.js", - "ui/panels/llm-config-panel.js", - "ui/panels/metrics-panel.js", - "ui/panels/vfs-panel.js", "ui/panels/cognition-panel.js", - "ui/components/confirmation-modal.js", - "ui/components/diff-viewer-ui.js", - "ui/components/toast-notifications.js", - "ui/components/hitl-widget.js", "ui/components/inline-chat.js", - "ui/dashboard/metrics-dashboard.js", - "ui/dashboard/ui-manager.js", - "ui/dashboard/vfs-explorer.js", - "ui/zero/index.js", - "ui/capsule/index.js" + "ui/zero/index.js" ], "styles": [ "styles/zero.css", "styles/capsule.css", - "styles/vfs-explorer.css", "styles/proto/index.css", "styles/rd.css", - "styles/landing-mono.css", "styles/proto/layout.css", "styles/proto/components.css", "styles/proto/history.css", @@ -385,7 +368,6 @@ "styles/proto/vfs.css", "styles/proto/responsive.css", "styles/proto/inline-chat.css", - "styles/proto/hitl.css", "styles/proto/optimization.css" ], "docs": [] diff --git a/self/config/genesis-template.json b/self/config/genesis-template.json index a229c1b14..509aefb8f 100644 --- a/self/config/genesis-template.json +++ b/self/config/genesis-template.json @@ -141,40 +141,23 @@ "tools/git.js" ], "ui": [ - "ui/proto.js", "ui/proto/index.js", "ui/proto/optimization.js", "ui/proto/template.js", "ui/proto/telemetry.js", - "ui/proto/schemas.js", "ui/proto/workers.js", "ui/proto/vfs.js", "ui/proto/utils.js", "ui/toast.js", - "ui/panels/chat-panel.js", - "ui/panels/code-panel.js", - "ui/panels/llm-config-panel.js", - "ui/panels/metrics-panel.js", - "ui/panels/vfs-panel.js", "ui/panels/cognition-panel.js", - "ui/components/confirmation-modal.js", - "ui/components/diff-viewer-ui.js", - "ui/components/toast-notifications.js", - "ui/components/hitl-widget.js", "ui/components/inline-chat.js", - "ui/dashboard/metrics-dashboard.js", - "ui/dashboard/ui-manager.js", - "ui/dashboard/vfs-explorer.js", - "ui/zero/index.js", - "ui/capsule/index.js" + "ui/zero/index.js" ], "styles": [ "styles/zero.css", "styles/capsule.css", - "styles/vfs-explorer.css", "styles/proto/index.css", "styles/rd.css", - "styles/landing-mono.css", "styles/proto/layout.css", "styles/proto/components.css", "styles/proto/history.css", @@ -182,7 +165,6 @@ "styles/proto/vfs.css", "styles/proto/responsive.css", "styles/proto/inline-chat.css", - "styles/proto/hitl.css", "styles/proto/optimization.css" ], "docs": [] diff --git a/self/config/lab-route-profiles.js b/self/config/lab-route-profiles.js deleted file mode 100644 index 3e2af8e60..000000000 --- a/self/config/lab-route-profiles.js +++ /dev/null @@ -1,5 +0,0 @@ -/** - * @fileoverview Compatibility re-export for the canonical lab profile registry. - */ - -export * from '../lab/profiles.js'; diff --git a/self/config/vfs-manifest.json b/self/config/vfs-manifest.json index 7ca7b5f22..c0f13ed85 100644 --- a/self/config/vfs-manifest.json +++ b/self/config/vfs-manifest.json @@ -1,6 +1,6 @@ { "version": 1, - "generatedAt": "2026-07-26T22:18:49.922Z", + "generatedAt": "2026-07-27T15:48:28.308Z", "files": [ "audit.html", "blueprint-index.json", @@ -358,7 +358,6 @@ "blueprints/0x00016a-config-clockwork-gamma-receipts.md", "blueprints/README.md", "blueprints/blueprint-index-contract.md", - "blueprints/implementation-status.md", "blueprints/promotion-contract.md", "blueprints/rgr-dream-instance-manifest.md", "blueprints/rgr-runtime-contract.md", @@ -408,7 +407,6 @@ "config/genesis-levels.json", "config/genesis-template.json", "config/immutability.js", - "config/lab-route-profiles.js", "config/lora-adapters/doppler-wgsl-qwen35-9b-v12-external20-seed11.json", "config/lora-adapters/doppler-wgsl-qwen35-9b-v12-external20-seed29.json", "config/lora-adapters/doppler-wgsl-qwen35-9b-v12-external20-seed47.json", @@ -425,7 +423,6 @@ "core/README.md", "core/agent-loop-policies.js", "core/agent-loop.js", - "core/async-utils.js", "core/context-manager.js", "core/cycle-artifacts.js", "core/doppler-runtime-service.js", @@ -489,7 +486,6 @@ "personas/code-architect.md", "personas/config.json", "pool-entry.html", - "pool/TODO.md", "pool/adapter-canary-publication.js", "pool/adapter-pack-publisher.js", "pool/adapter-pack.js", @@ -508,7 +504,6 @@ "pool/identity-claims.js", "pool/identity.js", "pool/inference-receipt.js", - "pool/layer-scheduler.js", "pool/model-artifacts.js", "pool/model-contract.js", "pool/p2p-artifact-transfer.js", @@ -522,12 +517,12 @@ "pool/peer-payload.js", "pool/peer-planning.js", "pool/peer-protocol.js", - "pool/peer-registry.js", "pool/peer-rendezvous.js", "pool/peer-room.js", "pool/peer-transport.js", "pool/points-ledger.js", "pool/policy-router.js", + "pool/policy-validation.js", "pool/pool-config.json", "pool/provider-client.js", "pool/reputation.js", @@ -536,7 +531,6 @@ "pool/sdk.js", "pool/sequence-result.js", "pool/sequence-workload.js", - "pool/shard-negotiation.js", "prompts/kernel.md", "providers/doppler-reploid.js", "reset.html", @@ -569,13 +563,11 @@ "styles/audit.css", "styles/boot.css", "styles/capsule.css", - "styles/landing-mono.css", "styles/poolday/components.css", "styles/poolday/primitives.css", "styles/poolday/tokens.css", "styles/proto/components.css", "styles/proto/history.css", - "styles/proto/hitl.css", "styles/proto/index.css", "styles/proto/inline-chat.css", "styles/proto/layout.css", @@ -584,7 +576,6 @@ "styles/proto/responsive.css", "styles/proto/vfs.css", "styles/rd.css", - "styles/vfs-explorer.css", "styles/zero.css", "sw-module-loader.js", "sw.js", @@ -592,8 +583,6 @@ "testing/arena/arena-harness.js", "testing/arena/arena-metrics.js", "testing/arena/competitor.js", - "testing/arena/doppler-integration.js", - "testing/arena/index.js", "testing/arena/vfs-sandbox.js", "tools/AwaitWorkers.js", "tools/CopyFile.js", @@ -625,7 +614,6 @@ "tools/Tail.js", "tools/WriteFile.js", "tools/git.js", - "ui/UI.js", "ui/boot-home/index.js", "ui/boot-wizard/detection.js", "ui/boot-wizard/goals.js", @@ -636,23 +624,11 @@ "ui/boot-wizard/steps/awaken.js", "ui/boot-wizard/steps/browser.js", "ui/boot-wizard/steps/choose.js", - "ui/boot-wizard/steps/detect.js", "ui/boot-wizard/steps/direct.js", "ui/boot-wizard/steps/goal.js", "ui/boot-wizard/steps/proxy.js", - "ui/boot-wizard/zero-function.js", - "ui/capsule/index.js", - "ui/components/arena-results.js", - "ui/components/confirmation-modal.js", - "ui/components/diff-viewer-ui.js", - "ui/components/hitl-widget.js", "ui/components/inline-chat.js", - "ui/components/toast-notifications.js", - "ui/dashboard/metrics-dashboard.js", - "ui/dashboard/ui-manager.js", - "ui/dashboard/vfs-explorer.js", "ui/panels/cognition-panel.js", - "ui/panels/metrics-panel.js", "ui/pool-home/constants.js", "ui/pool-home/contribution-state.js", "ui/pool-home/controls.js", @@ -668,11 +644,9 @@ "ui/pool-home/simulation-frame-state.js", "ui/pool-home/simulation-renderer.js", "ui/pool-home/view.js", - "ui/proto.js", "ui/proto/index.js", "ui/proto/optimization.js", "ui/proto/replay.js", - "ui/proto/schemas.js", "ui/proto/telemetry.js", "ui/proto/template.js", "ui/proto/utils.js", diff --git a/self/core/async-utils.js b/self/core/async-utils.js deleted file mode 100644 index fa6904a55..000000000 --- a/self/core/async-utils.js +++ /dev/null @@ -1,263 +0,0 @@ -/** - * @fileoverview Async Utilities for Reploid - * - * Provides timeout, retry, and resilience patterns for async operations. - * Used by tools that make network calls, long-running operations, or - * operations that could fail transiently. - */ - -/** - * Wrap a promise with a timeout - * @param {Promise} promise - The promise to wrap - * @param {number} timeoutMs - Timeout in milliseconds - * @param {string} [operationName] - Name for error messages - * @returns {Promise} - Resolves with result or rejects with TimeoutError - */ -export function withTimeout(promise, timeoutMs, operationName = 'Operation') { - return new Promise((resolve, reject) => { - const timeoutId = setTimeout(() => { - reject(new TimeoutError(`${operationName} timed out after ${timeoutMs}ms`)); - }, timeoutMs); - - promise - .then((result) => { - clearTimeout(timeoutId); - resolve(result); - }) - .catch((error) => { - clearTimeout(timeoutId); - reject(error); - }); - }); -} - -/** - * Custom error class for timeouts - */ -export class TimeoutError extends Error { - constructor(message) { - super(message); - this.name = 'TimeoutError'; - this.isTimeout = true; - } -} - -/** - * Custom error class for retry exhaustion - */ -export class RetryExhaustedError extends Error { - constructor(message, attempts, lastError) { - super(message); - this.name = 'RetryExhaustedError'; - this.attempts = attempts; - this.lastError = lastError; - } -} - -/** - * Retry an async operation with exponential backoff - * @param {Function} fn - Async function to retry - * @param {Object} options - Retry options - * @param {number} [options.maxAttempts=3] - Maximum retry attempts - * @param {number} [options.initialDelayMs=1000] - Initial delay between retries - * @param {number} [options.maxDelayMs=30000] - Maximum delay between retries - * @param {number} [options.backoffMultiplier=2] - Exponential backoff multiplier - * @param {Function} [options.shouldRetry] - Predicate to determine if retry should occur - * @param {Function} [options.onRetry] - Callback on each retry attempt - * @returns {Promise} - Result of successful execution - */ -export async function withRetry(fn, options = {}) { - const { - maxAttempts = 3, - initialDelayMs = 1000, - maxDelayMs = 30000, - backoffMultiplier = 2, - shouldRetry = () => true, - onRetry = null - } = options; - - let lastError; - let delayMs = initialDelayMs; - - for (let attempt = 1; attempt <= maxAttempts; attempt++) { - try { - return await fn(attempt); - } catch (error) { - lastError = error; - - // Check if we should retry - if (attempt === maxAttempts || !shouldRetry(error, attempt)) { - break; - } - - // Notify retry callback - if (onRetry) { - onRetry(error, attempt, delayMs); - } - - // Wait before retrying - await sleep(delayMs); - - // Exponential backoff - delayMs = Math.min(delayMs * backoffMultiplier, maxDelayMs); - } - } - - throw new RetryExhaustedError( - `Operation failed after ${maxAttempts} attempts`, - maxAttempts, - lastError - ); -} - -/** - * Combine timeout and retry for robust async operations - * @param {Function} fn - Async function to execute - * @param {Object} options - Combined options - * @param {number} [options.timeoutMs=30000] - Timeout per attempt - * @param {number} [options.maxAttempts=3] - Maximum retry attempts - * @param {number} [options.initialDelayMs=1000] - Initial retry delay - * @param {string} [options.operationName] - Name for error messages - * @param {Function} [options.shouldRetry] - Predicate for retry - * @param {Function} [options.onRetry] - Retry callback - * @returns {Promise} - Result of successful execution - */ -export async function withTimeoutAndRetry(fn, options = {}) { - const { - timeoutMs = 30000, - operationName = 'Operation', - ...retryOptions - } = options; - - return withRetry( - async (attempt) => { - return withTimeout(fn(attempt), timeoutMs, `${operationName} (attempt ${attempt})`); - }, - { - ...retryOptions, - // Always retry on timeout - shouldRetry: (error, attempt) => { - if (error.isTimeout) return true; - if (retryOptions.shouldRetry) return retryOptions.shouldRetry(error, attempt); - return isTransientError(error); - } - } - ); -} - -/** - * Check if an error is likely transient and worth retrying - * @param {Error} error - The error to check - * @returns {boolean} - */ -export function isTransientError(error) { - // Network errors - if (error.name === 'TypeError' && error.message.includes('fetch')) return true; - if (error.name === 'NetworkError') return true; - if (error.message?.includes('network')) return true; - if (error.message?.includes('ECONNREFUSED')) return true; - if (error.message?.includes('ETIMEDOUT')) return true; - if (error.message?.includes('ENOTFOUND')) return true; - - // Timeout errors - if (error.isTimeout) return true; - - // HTTP 5xx errors - if (error.status >= 500 && error.status < 600) return true; - if (error.code === 503) return true; // Service unavailable - if (error.code === 429) return true; // Too many requests - - // Worker errors - if (error.message?.includes('worker')) return true; - - return false; -} - -/** - * Sleep for a specified duration - * @param {number} ms - Duration in milliseconds - * @returns {Promise} - */ -export function sleep(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); -} - -/** - * Create a deferred promise with external resolve/reject - * @returns {{promise: Promise, resolve: Function, reject: Function}} - */ -export function createDeferred() { - let resolve, reject; - const promise = new Promise((res, rej) => { - resolve = res; - reject = rej; - }); - return { promise, resolve, reject }; -} - -/** - * Race a promise against a timeout with cancellation support - * @param {Function} fn - Function that returns a promise - * @param {number} timeoutMs - Timeout in milliseconds - * @param {Object} [options] - Options - * @param {Function} [options.onCancel] - Called when timeout triggers (for cleanup) - * @returns {Promise} - */ -export async function raceWithTimeout(fn, timeoutMs, options = {}) { - const { onCancel } = options; - const abortController = new AbortController(); - - const timeoutPromise = new Promise((_, reject) => { - setTimeout(() => { - abortController.abort(); - if (onCancel) onCancel(); - reject(new TimeoutError(`Operation timed out after ${timeoutMs}ms`)); - }, timeoutMs); - }); - - return Promise.race([ - fn(abortController.signal), - timeoutPromise - ]); -} - -/** - * Execute multiple promises with individual timeouts - * @param {Array<{fn: Function, timeoutMs: number, name: string}>} tasks - Tasks to execute - * @param {Object} [options] - Options - * @param {boolean} [options.continueOnError=false] - Continue if some tasks fail - * @returns {Promise>} - */ -export async function executeWithTimeouts(tasks, options = {}) { - const { continueOnError = false } = options; - - const results = await Promise.allSettled( - tasks.map(async (task) => { - try { - const result = await withTimeout(task.fn(), task.timeoutMs, task.name); - return { name: task.name, result }; - } catch (error) { - if (!continueOnError) throw error; - return { name: task.name, error }; - } - }) - ); - - return results.map((r, i) => { - if (r.status === 'fulfilled') return r.value; - return { name: tasks[i].name, error: r.reason }; - }); -} - -export default { - withTimeout, - withRetry, - withTimeoutAndRetry, - raceWithTimeout, - executeWithTimeouts, - isTransientError, - sleep, - createDeferred, - TimeoutError, - RetryExhaustedError -}; diff --git a/self/host/seed-vfs.js b/self/host/seed-vfs.js index 754d9296e..b6e312656 100644 --- a/self/host/seed-vfs.js +++ b/self/host/seed-vfs.js @@ -221,8 +221,7 @@ const WARM_BOOT_PROBE_PATHS = Object.freeze([ '/host/start-app.js', '/lab/profiles.js', '/lab/mirrors.js', - '/config/boot-seed.js', - '/config/lab-route-profiles.js' + '/config/boot-seed.js' ]); const shouldUseServiceWorkerForBoot = (bootProfile) => SERVICE_WORKER_BOOT_PROFILES.has(bootProfile); diff --git a/self/lab/mirrors.js b/self/lab/mirrors.js index 947f65305..1b6f86e13 100644 --- a/self/lab/mirrors.js +++ b/self/lab/mirrors.js @@ -5,7 +5,6 @@ export const ZERO_RUNTIME_SELF_MIRROR_RULES = Object.freeze([ { sourcePrefix: '/boot-helpers/', targetPrefix: '/self/boot-helpers/' }, { sourcePrefix: '/capabilities/', targetPrefix: '/self/capabilities/' }, - { sourcePath: '/config/lab-route-profiles.js', targetPath: '/self/config/lab-route-profiles.js' }, { sourcePath: '/config/tool-surfaces.js', targetPath: '/self/config/tool-surfaces.js' }, { sourcePrefix: '/core/', targetPrefix: '/self/core/' }, { sourcePrefix: '/host/', targetPrefix: '/self/host/' }, diff --git a/self/manifest.js b/self/manifest.js index d7973c480..1c5e45b57 100644 --- a/self/manifest.js +++ b/self/manifest.js @@ -32,7 +32,6 @@ export const SELF_SOURCE_MIRRORS = Object.freeze([ { webPath: '/runtime.js', vfsPath: '/self/runtime.js' }, { webPath: '/bridge.js', vfsPath: '/self/bridge.js' }, { webPath: '/tool-runner.js', vfsPath: '/self/tool-runner.js' }, - { webPath: '/config/lab-route-profiles.js', vfsPath: '/self/config/lab-route-profiles.js' }, { webPath: '/config/tool-surfaces.js', vfsPath: '/self/config/tool-surfaces.js' }, { webPath: '/config/vfs-policy.js', vfsPath: '/self/config/vfs-policy.js' }, { webPath: '/lab/mirrors.js', vfsPath: '/self/lab/mirrors.js' }, @@ -85,6 +84,7 @@ export const SELF_SOURCE_MIRRORS = Object.freeze([ { webPath: '/pool/p2p-signaling.js', vfsPath: '/self/pool/p2p-signaling.js' }, { webPath: '/pool/p2p-transport.js', vfsPath: '/self/pool/p2p-transport.js' }, { webPath: '/pool/policy-router.js', vfsPath: '/self/pool/policy-router.js' }, + { webPath: '/pool/policy-validation.js', vfsPath: '/self/pool/policy-validation.js' }, { webPath: '/pool/provider-client.js', vfsPath: '/self/pool/provider-client.js' }, { webPath: '/pool/requester-client.js', vfsPath: '/self/pool/requester-client.js' }, { webPath: '/pool/reputation.js', vfsPath: '/self/pool/reputation.js' }, diff --git a/self/pool/TODO.md b/self/pool/TODO.md deleted file mode 100644 index 376c78551..000000000 --- a/self/pool/TODO.md +++ /dev/null @@ -1,157 +0,0 @@ -# Reploid Pool TODO - -Reploid is the market-facing browser inference network and governed browser substrate. Browser providers serve model runs through signed peer intents, WebRTC payload transit, receipts, verification, reputation, and requester acceptance. Public-facing product copy should use Reploid. - -Canonical claim and deployment truth stay in [`pool-config.json`](./pool-config.json). Architecture and production contracts stay in [`../../docs/browser-inference-pool.md`](../../docs/browser-inference-pool.md). - -The current Cloud Run and Firestore path is transitional. The target Reploid control plane is WebRTC peer-to-peer: signed job intents, provider capability adverts, assignment selection, quorum agreement, receipts, acceptance, points, and reputation should move without a required Reploid server. - ---- - -## Current Local Checks - -- [x] `npm run verify:pool -- --allow-placeholders` passes locally. -- [x] Unit suite passes: 1,511 passed and 25 skipped. -- [x] Integration suite passes: 359 passed and 9 skipped. -- [x] `npm run verify:pool:release -- --url https://reploid.web.app --channel=chrome` passes production readiness, all public routes, synthetic peer flow, actual Doppler WebGPU inference, receipt agreement, requester signature verification, and signed points/reputation event verification. -- [x] `npm audit` and the production-image `npm ci --omit=dev --include=optional` audit report zero vulnerabilities. -- [x] No literal `TODO`, `FIXME`, `TBD`, or `XXX` markers existed in pool files before this document. - ---- - -## Source Of Truth - -| Surface | Path | Purpose | -|---------|------|---------| -| Product config | [`pool-config.json`](./pool-config.json) | Product-owned claim, launch model, trust tiers, policies, routes, transport, and deployment requirements. | -| Product doc | [`../../docs/browser-inference-pool.md`](../../docs/browser-inference-pool.md) | Public architecture, API contract, production readiness, and forbidden claims. | -| Product UI | [`../ui/pool-home/index.js`](../ui/pool-home/index.js) | `/`, `/ask`, `/compute`, `/history`, `/network`, and `/zero` browser surface. | -| Peer control plane | [`peer-control-plane.js`](./peer-control-plane.js) | Signed peer messages, deterministic assignment planning, DataChannel bus helpers, and peer reducers. | -| Peer room | [`peer-room.js`](./peer-room.js) | Browser room bootstrap for primary `/ask` and `/compute` flows without hosted job or provider assignment calls. | -| Coordinator | [`../../server/pool/routes.js`](../../server/pool/routes.js) | Cloud Run routes for config, jobs, providers, receipts, reputation, signaling, and deployment check. | -| Verification script | [`../../scripts/verify-pool-production.js`](../../scripts/verify-pool-production.js) | Static, route, config, and hosted readiness verification. | - ---- - -## Launch Proof - -- [x] Deploy Reploid public hosting plus the Reploid Cloud Run coordinator with `POOL_BACKEND_ONLY=true`, `POOL_STORE=firestore`, Firebase Auth verification, required rewrites, commit-reveal support, and metadata-only signaling. -- [x] Run production verification against `https://reploid.web.app`; `/pool/deployment/check` returns `ok: true` for config `2026-07-24.doppler-0.4.16.v1` (`sha256:f15ad298576206e54a4448c424b694bb457960f244d08673d0445f90966a2836`), Firestore, required auth, artifact base, and commit-reveal support. -- [x] Run public smoke against `https://reploid.web.app` and cover `/`, `/ask`, `/compute`, `/records`, `/history`, `/network`, and `/zero` plus the synthetic peer flow. -- [x] Prove the primary WebRTC loop on the hosted surface: requester intent, provider model load and advert, deterministic assignment, real Doppler generation, signed receipt agreement, verifier decision, requester acceptance, points event, and reputation event. -- [ ] Prove the separate optional hosted diagnostic loop through provider registration, assignment claim, commit, reveal, and expired-assignment recovery. - ---- - -## Decentralized Control Plane - -- [x] Define signed peer-message envelopes for job intent, provider advert, assignment claim, commit, reveal, execution result, receipt, acceptance, points event, reputation event, and peer heartbeat. -- [x] Add signed provider capability adverts that bind identity, model, manifest, runtime profile, accepted policies, availability, and reputation evidence. -- [x] Add deterministic local assignment selection from intent hash, provider adverts, policy, runtime profile, model identity, and reputation evidence. -- [x] Add a browser peer room that replaces server-created jobs and hosted provider assignment polling for the primary `/ask` and `/compute` flow. -- [x] Add browser-room ring quorum agreement from matching receipt hashes over WebRTC provider sessions. -- [x] Add signed peer ledger events for accepted receipt sets plus deterministic points and reputation reducers. -- [x] Replace server-created jobs with requester signed intents across `/ask`, `/compute`, and quorum policies. -- [x] Replace coordinator signaling dependency for primary routes with peer-discovered WebRTC sessions; optional server relay is bootstrap only, not control-plane authority. -- [x] Gossip accepted receipt sets, points events, and reputation events inside local and relayed peer rooms. -- [ ] Gossip accepted receipt sets, points events, and reputation events across a true serverless wide-area WebRTC peer graph beyond room relay. -- [x] Keep optional public anchors for auditability, but do not require a Reploid server to create jobs, assign providers, decide consensus, or mutate reputation. - ---- - -## Model Artifact Path - -- [x] Publish and pin launch model artifacts under the selected model's configured `artifactPolicy.baseUrl`, with `REPLOID_POOL_MODEL_BASE_URL` as an override for alternate artifact roots. -- [x] Add strict artifact manifest preflight for CORS fetch, manifest JSON, manifest hash, model id, and model hash. -- [x] Verify tokenizer and shard identities, independent HTTP range requests, and cold-to-warm OPFS cache reuse against the published artifact host. -- [x] Make strict-preflight artifact failures legible in `/compute`: missing manifest, hash mismatch, CORS denial, and unsupported browser runtime. -- [x] Keep model bytes out of Firebase Hosting and Cloud Run; the production verifier rejects bundled weight formats and requires external content-addressed HTTPS artifact roots. - ---- - -## Doppler Evidence Contract - -- [x] Pin npm tooling and the immutable browser runtime to published `doppler-gpu@0.5.1`; verify the exact tarball integrity and npm jsDelivr entry without import-map or bundler assumptions. -- [x] Consume the narrow public Doppler evidence export for token ids, transcript hashes, generation config, runtime profile hash, and backend identity. -- [x] Keep Reploid from deep-importing Doppler internals. -- [x] Show a visible comparison receipt for Doppler output fields versus Reploid receipt fields. -- [x] Use the configured public `generateWithEvidence` export without a token-level warning and assert the full evidence comparison; retain the warning only for unsupported third-party handles. - ---- - -## Provider Supply - -- [x] Make `/compute` primary Start load the model, create a signed provider advert, and listen for peer-room WebRTC jobs. -- [x] Keep the hosted manual provider controls coherent: register, heartbeat, poll, execute, commit, reveal, and submit receipt. -- [x] Surface provider health states: WebGPU unavailable, model loading, artifact failure, storage quota, queue state, last receipt, trust tier, and reputation. -- [x] Test multiple same-origin browser-room providers on the same launch model and runtime profile through a ring quorum policy. -- [x] Add browser smoke coverage that opens provider and requester pages, injects a deterministic browser runtime, and proves visible peer receipt flow. -- [x] Test multiple real browser tabs on the published launch model artifacts and runtime profile through a ring quorum policy. -- [x] Add provider hardening for duplicate peer sessions, provider busy rejection, stopped nodes, and completed session cleanup. -- [x] Restore an opted-in peer provider after refresh or tab visibility recovery with the same role identity and warm OPFS model. -- [ ] Recover hosted diagnostic assignments after expiration or a reveal miss. - ---- - -## Requester And Agent Demand - -- [x] Make `/ask` create a signed peer intent, discover multiple provider adverts for ring policies, send prompts over DataChannel, receive receipts, form quorum, countersign acceptance locally, and gossip signed ledger events to providers. -- [x] Make `/ask` state the exact trust tier and receipt status in user language without forbidden claims. -- [x] Capture route and rejection decisions and expose them in receipt history. -- [x] Show requester-visible spend, agreement threshold, verifier decision, model identity, runtime identity, output hash, token hash status, and provider signature. - ---- - -## Security And Abuse - -- [x] Lock Firebase Auth role binding on requester, agent, provider, and verifier identities. -- [x] Verify direct Firestore access is denied outside declared server-mediated flows. -- [x] Exercise peer-room relay metadata-only limits, payload caps, TTLs, peer filtering, and rejection of prompt/output/receipt/model payloads. -- [x] Exercise the distributed Firestore rate window against deployed Firebase/Cloud Run and require the expected accepted-versus-limited burst result. -- [ ] Capture deployed expiration and stale-peer cleanup evidence for the optional hosted signaling path. -- [x] Enforce a Firestore-transaction-backed per-client rate limit across hosted pool routes, including job, heartbeat, signaling, and receipt endpoints. -- [x] Add production evidence for Firestore rules, Cloud Run auth handling, and hosted route rewrites. - ---- - -## Strategic Wedge - -- [x] Keep the public front-door sentence: `Run browser models together.` -- [x] Treat external artifact storage as interchangeable byte delivery. Reploid owns product execution, receipts, verification, reputation, requester acceptance, and the browser substrate. -- [x] Position Doppler as the browser inference engine. Reploid is the decentralized serving product and governed browser substrate. -- [x] Treat WebRTC as both the target control plane and the default prompt/output/receipt transit. -- [x] Avoid forbidden claims: `trustless`, `hardware-attested`, `guaranteed honest GPU execution`, and `decentralized AI compute marketplace at launch`. -- [x] Optimize for one public proof that a browser can do useful model work, produce an inspectable receipt, earn reputation, and serve an agent or requester. - ---- - -## Explicit Non-Goals - -- [ ] Do not launch paid settlement or payouts before accepted receipts and reputation work publicly. -- [ ] Do not claim hardware attestation. -- [ ] Do not make broad `/pool/**` Firebase backend rewrites. -- [x] Do not deep-import Doppler internals. -- [ ] Do not let UI copy exceed `pool-config.json` claims. -- [x] Present Reploid as the public product brand and substrate identity. -- [ ] Do not treat Cloud Run or Firestore as the permanent Reploid authority. - ---- - -## Done Definition - -- [x] Deployed `/pool/deployment/check` returns `ok: true`. -- [x] Public smoke passes against the hosted surface. -- [x] The browser-room code path can run `/compute` providers and `/ask` requester logic without coordinator job creation, collect accepted receipts, and reduce signed points plus reputation events locally. -- [x] A browser smoke can open `/compute` and `/ask`, receive an accepted receipt, and expose local points plus reputation projection in the visible UI. -- [x] A user can do the same against published model artifacts on the hosted surface. -- [x] Prompt, output, and full receipt payloads move over WebRTC DataChannel by default, with coordinator signaling restricted to WebRTC metadata. -- [x] Browser-room ring policy agreement happens through WebRTC provider sessions and produces accepted receipt sets plus agreement hashes. -- [x] Same-origin browser-room target path works without required Reploid server control-plane calls: peers discover local adverts, route signed intents, elect providers, reach quorum, countersign acceptance, and produce signed reputation events. -- [x] Wider room path works with optional metadata relay and without required Reploid server job, assignment, quorum, acceptance, points, or reputation authority. -- [ ] Wider peer graph path works without any Reploid server relay across remote browsers. -- [x] The receipt binds model hash, manifest hash, runtime, backend, generation config, output hash, token ids hash or documented warning, provider signature, verifier decision, and requester acceptance. -- [x] Docs, config, UI copy, and verifier claims match. - ---- - -*Last updated: July 26, 2026* diff --git a/self/pool/layer-scheduler.js b/self/pool/layer-scheduler.js deleted file mode 100644 index 33e38d4eb..000000000 --- a/self/pool/layer-scheduler.js +++ /dev/null @@ -1,171 +0,0 @@ -/** - * @fileoverview Layer assignment and scheduling for Reploid pipeline-parallel execution. - * Section 3 of TODO_REPLOID.md: Layer Assignment and Scheduling. - * - * Implements: - * - Critical path optimization (TTFT: lowest RTT and highest reliability for prefill layers) - * - Warm standby policy (active P1 + standby P2 per layer group) - * - Structured assignment logging written into the final receipt - */ - -export const LAYER_SCHEDULER_VERSION = 'reploid_layer_scheduler/v1'; - -const PREFILL_LAYER_THRESHOLD = 4; -const TTFT_MAX_RTT_MS = 20; -const TTFT_MIN_RELIABILITY = 0.99; -const TIMEOUT_RELIABILITY_PENALTY = 0.05; - -export function buildLayerGroups({ totalLayers, peerCount }) { - const count = Math.max(1, Math.floor(peerCount)); - const layers = Math.max(1, Math.floor(totalLayers)); - const groupSize = Math.ceil(layers / count); - const groups = []; - for (let start = 0; start < layers; start += groupSize) { - const end = Math.min(start + groupSize - 1, layers - 1); - groups.push({ layerStart: start, layerEnd: end, layers: Array.from({ length: end - start + 1 }, (_, i) => start + i) }); - } - return groups; -} - -function scorePeerForGroup(peer, groupIndex, { isPrefill = false } = {}) { - const rtt = Number(peer.network_performance?.latency_rtt_ms ?? 999); - const reliability = Number(peer.reliability_score ?? 0); - const hasWebGPU = (peer.hardware_capabilities?.backends || []).includes('webgpu'); - - if (isPrefill) { - if (rtt > TTFT_MAX_RTT_MS || reliability < TTFT_MIN_RELIABILITY) return -Infinity; - } - - const rttScore = Math.max(0, 1 - rtt / 200); - const reliabilityScore = reliability; - const backendBonus = hasWebGPU ? 0.1 : 0; - - return rttScore * 0.4 + reliabilityScore * 0.5 + backendBonus; -} - -export function assignLayerGroups({ peers, layerGroups, standbyDepth = 1 }) { - if (!Array.isArray(peers) || peers.length === 0) throw new Error('peers must be a non-empty array'); - if (!Array.isArray(layerGroups) || layerGroups.length === 0) throw new Error('layerGroups must be a non-empty array'); - - const assignments = []; - const assignedAsActive = new Set(); - - for (let gi = 0; gi < layerGroups.length; gi++) { - const group = layerGroups[gi]; - const isPrefill = group.layerStart <= PREFILL_LAYER_THRESHOLD; - - const scored = peers - .map((p) => ({ peer: p, score: scorePeerForGroup(p, gi, { isPrefill }) })) - .filter((e) => Number.isFinite(e.score)) - .sort((a, b) => b.score - a.score); - - if (!scored.length) throw new Error(`no eligible peer for layer group ${gi} (layers ${group.layerStart}..${group.layerEnd})`); - - const active = scored[0].peer; - assignedAsActive.add(active.peer_id); - - const standbyPool = scored.slice(1).filter((e) => !assignedAsActive.has(e.peer.peer_id)); - const standbys = standbyPool.slice(0, standbyDepth).map((e) => e.peer); - - assignments.push({ - groupIndex: gi, - layerStart: group.layerStart, - layerEnd: group.layerEnd, - layers: group.layers, - isPrefill, - activePeer: active, - standbyPeers: standbys, - score: scored[0].score, - assignedAt: new Date().toISOString() - }); - } - - return assignments; -} - -export function buildAssignmentLog({ assignments, sessionId, jobId }) { - const entries = assignments.map((a) => ({ - groupIndex: a.groupIndex, - layerStart: a.layerStart, - layerEnd: a.layerEnd, - isPrefill: a.isPrefill, - activePeerId: a.activePeer?.peer_id, - standbyPeerIds: (a.standbyPeers || []).map((p) => p.peer_id), - reliabilityAtAssignment: a.activePeer?.reliability_score, - rttAtAssignment: a.activePeer?.network_performance?.latency_rtt_ms, - score: a.score, - assignedAt: a.assignedAt - })); - - return Object.freeze({ - schedulerVersion: LAYER_SCHEDULER_VERSION, - sessionId: String(sessionId || ''), - jobId: String(jobId || ''), - totalGroups: assignments.length, - entries: Object.freeze(entries), - loggedAt: new Date().toISOString() - }); -} - -export function createWarmStandbyMonitor({ assignments, onFailover, heartbeatIntervalMs = 3000 }) { - const active = new Map(assignments.map((a) => [a.groupIndex, a])); - const failedOver = new Set(); - let intervalId = null; - const lastSeen = new Map(assignments.map((a) => [a.activePeer?.peer_id, Date.now()])); - - const heartbeat = (peerId) => { - lastSeen.set(peerId, Date.now()); - }; - - const check = () => { - const now = Date.now(); - for (const [gi, assignment] of active) { - if (failedOver.has(gi)) continue; - const pid = assignment.activePeer?.peer_id; - const seen = lastSeen.get(pid) ?? 0; - if (now - seen > heartbeatIntervalMs * 2) { - failedOver.add(gi); - const standby = assignment.standbyPeers?.[0] ?? null; - if (typeof onFailover === 'function') { - onFailover({ groupIndex: gi, failedPeerId: pid, standbyPeer: standby, at: new Date().toISOString() }); - } - } - } - }; - - const start = () => { - if (intervalId) return; - intervalId = setInterval(check, heartbeatIntervalMs); - }; - - const stop = () => { - if (intervalId) clearInterval(intervalId); - intervalId = null; - }; - - return { start, stop, heartbeat, isFailedOver: (gi) => failedOver.has(gi) }; -} - -export function computeDeadlineMs({ prefillTimeMs, rttMs }) { - return Math.max(0, Number(prefillTimeMs || 0)) + 1.5 * Math.max(0, Number(rttMs || 0)); -} - -export function applyTimeoutPenalty({ registry, peerId }) { - if (typeof registry?.updateReliability === 'function') { - registry.updateReliability(peerId, -TIMEOUT_RELIABILITY_PENALTY); - } - return { peerId, delta: -TIMEOUT_RELIABILITY_PENALTY }; -} - -export default { - LAYER_SCHEDULER_VERSION, - PREFILL_LAYER_THRESHOLD, - TTFT_MAX_RTT_MS, - TTFT_MIN_RELIABILITY, - buildLayerGroups, - assignLayerGroups, - buildAssignmentLog, - createWarmStandbyMonitor, - computeDeadlineMs, - applyTimeoutPenalty -}; diff --git a/self/pool/peer-registry.js b/self/pool/peer-registry.js deleted file mode 100644 index dd3cf7e09..000000000 --- a/self/pool/peer-registry.js +++ /dev/null @@ -1,144 +0,0 @@ -/** - * @fileoverview Peer capability registry for Reploid orchestration. - * Section 1 of TODO_REPLOID.md: Peer Registry and Capability Schema. - */ - -export const PEER_REGISTRY_VERSION = 'reploid_peer_registry/v1'; - -const VALID_BACKENDS = new Set(['webgpu', 'metal', 'vulkan', 'dx12', 'cpu']); -const VALID_GENERATORS = new Set([ - 'splitmix64_normal_v1', - 'siren_f16_v1', - 'siren_f32_v1' -]); - -const cleanString = (v) => String(v || '').trim() || null; -const cleanNumber = (v, fallback = 0) => (Number.isFinite(Number(v)) ? Number(v) : fallback); -const cleanArray = (v) => (Array.isArray(v) ? v.map(String) : []); - -export function buildCapabilityProfile({ - peerId, - reploidVersion = 'reploid@0.22.4', - hardwareCapabilities = {}, - networkPerformance = {}, - reliabilityScore = null -} = {}) { - const id = cleanString(peerId); - if (!id) throw new TypeError('peerId is required'); - return Object.freeze({ - registryVersion: PEER_REGISTRY_VERSION, - peer_id: id, - reploid_version: cleanString(reploidVersion) || 'reploid@0.0.0', - hardware_capabilities: Object.freeze({ - available_vram_bytes: cleanNumber(hardwareCapabilities.available_vram_bytes), - backends: Object.freeze(cleanArray(hardwareCapabilities.backends).filter((b) => VALID_BACKENDS.has(b))), - supported_generators: Object.freeze(cleanArray(hardwareCapabilities.supported_generators).filter((g) => VALID_GENERATORS.has(g))) - }), - network_performance: Object.freeze({ - bandwidth_ingress_bps: cleanNumber(networkPerformance.bandwidth_ingress_bps), - bandwidth_egress_bps: cleanNumber(networkPerformance.bandwidth_egress_bps), - latency_rtt_ms: cleanNumber(networkPerformance.latency_rtt_ms) - }), - reliability_score: reliabilityScore !== null && reliabilityScore !== undefined - ? Math.max(0, Math.min(1, Number(reliabilityScore))) - : 1.0, - registered_at: new Date().toISOString() - }); -} - -export function validateCapabilityProfile(profile = {}) { - const reasons = []; - if (!cleanString(profile.peer_id)) reasons.push('peer_id is required'); - if (!cleanString(profile.reploid_version)) reasons.push('reploid_version is required'); - if (!profile.hardware_capabilities || typeof profile.hardware_capabilities !== 'object') { - reasons.push('hardware_capabilities must be an object'); - } else { - if (!cleanArray(profile.hardware_capabilities.backends).length) { - reasons.push('hardware_capabilities.backends must not be empty'); - } - } - if (!profile.network_performance || typeof profile.network_performance !== 'object') { - reasons.push('network_performance must be an object'); - } else { - if (cleanNumber(profile.network_performance.latency_rtt_ms) < 0) { - reasons.push('network_performance.latency_rtt_ms must be >= 0'); - } - } - if (typeof profile.reliability_score !== 'number' || profile.reliability_score < 0 || profile.reliability_score > 1) { - reasons.push('reliability_score must be a number in [0, 1]'); - } - return { ok: reasons.length === 0, reasons }; -} - -export function createPeerRegistry() { - const profiles = new Map(); - const quarantined = new Set(); - const blocked = new Set(); - - const register = (profile) => { - const { ok, reasons } = validateCapabilityProfile(profile); - if (!ok) throw new Error(`invalid capability profile: ${reasons.join('; ')}`); - profiles.set(profile.peer_id, { ...profile, last_seen: new Date().toISOString() }); - return profile.peer_id; - }; - - const unregister = (peerId) => { - profiles.delete(peerId); - }; - - const get = (peerId) => profiles.get(peerId) || null; - - const updateReliability = (peerId, delta) => { - const p = profiles.get(peerId); - if (!p) return; - const next = Math.max(0, Math.min(1, p.reliability_score + delta)); - profiles.set(peerId, { ...p, reliability_score: next }); - if (next < 0.5) { - blocked.add(peerId); - } - }; - - const quarantine = (peerId, reason = 'output_mismatch') => { - quarantined.add(peerId); - updateReliability(peerId, -0.25); - return { peerId, reason, quarantinedAt: new Date().toISOString() }; - }; - - const isEligible = (peerId) => !quarantined.has(peerId) && !blocked.has(peerId); - - const listEligible = ({ minReliability = 0, requireBackend = null } = {}) => { - const result = []; - for (const [id, p] of profiles) { - if (!isEligible(id)) continue; - if (p.reliability_score < minReliability) continue; - if (requireBackend && !p.hardware_capabilities.backends.includes(requireBackend)) continue; - result.push(p); - } - return result.sort((a, b) => b.reliability_score - a.reliability_score); - }; - - const clearQuarantine = (peerId) => { - quarantined.delete(peerId); - }; - - return { - register, - unregister, - get, - updateReliability, - quarantine, - clearQuarantine, - isEligible, - listEligible, - isBlocked: (id) => blocked.has(id), - isQuarantined: (id) => quarantined.has(id), - size: () => profiles.size - }; -} - -export default { - PEER_REGISTRY_VERSION, - buildCapabilityProfile, - validateCapabilityProfile, - createPeerRegistry -}; diff --git a/self/pool/policy-router.js b/self/pool/policy-router.js index 29728a1cc..4a92b9b34 100644 --- a/self/pool/policy-router.js +++ b/self/pool/policy-router.js @@ -11,6 +11,12 @@ import { listPolicies } from './config.js'; import { validateLaunchModelRequirement } from './model-contract.js'; +import { + POOLDAY_POLICY_CLASSES, + classifyPooldayPrompt, + validateGenerationConfig, + validatePooldayPolicyClasses +} from './policy-validation.js'; export { DETERMINISTIC_GENERATION_CONFIG, @@ -18,76 +24,18 @@ export { LAUNCH_POLICIES, POLICY_IDS, getPolicy, - listPolicies + listPolicies, + POOLDAY_POLICY_CLASSES, + classifyPooldayPrompt, + validatePooldayPolicyClasses }; export const FASTEST_RECEIPT_POLICY = LAUNCH_POLICIES[POLICY_IDS.fastestReceipt]; export const CANARY_AUDITED_POLICY = LAUNCH_POLICIES[POLICY_IDS.canaryAudited]; export const REDUNDANT_AGREEMENT_POLICY = LAUNCH_POLICIES[POLICY_IDS.redundantAgreement]; export const RING_QUORUM_RECEIPT_POLICY = LAUNCH_POLICIES[POLICY_IDS.ringQuorumReceipt]; -export const POOLDAY_POLICY_CLASSES = Object.freeze({ - publicText: 'public_text', - codeHelp: 'code_help', - benchmarkEval: 'benchmark_eval', - pii: 'pii', - secrets: 'secrets', - medicalPrivate: 'medical_private', - illegalContent: 'illegal_content' -}); - -const BLOCKED_PUBLIC_PROVIDER_CLASSES = new Set([ - POOLDAY_POLICY_CLASSES.pii, - POOLDAY_POLICY_CLASSES.secrets, - POOLDAY_POLICY_CLASSES.medicalPrivate, - POOLDAY_POLICY_CLASSES.illegalContent -]); - -export function classifyPooldayPrompt(prompt = '') { - const text = String(prompt || ''); - const classes = new Set([POOLDAY_POLICY_CLASSES.publicText]); - if (/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/i.test(text)) classes.add(POOLDAY_POLICY_CLASSES.pii); - if (/\b(api[_-]?key|secret|password|private[_-]?key|token)\b\s*[:=]/i.test(text)) classes.add(POOLDAY_POLICY_CLASSES.secrets); - if (/\b(sk-[a-z0-9]{12,}|AIza[0-9A-Za-z_-]{20,})\b/.test(text)) classes.add(POOLDAY_POLICY_CLASSES.secrets); - if (/\b(patient|diagnosis|medical record|prescription)\b/i.test(text)) classes.add(POOLDAY_POLICY_CLASSES.medicalPrivate); - if (/\b(malware|credential theft|phishing kit|exploit chain)\b/i.test(text)) classes.add(POOLDAY_POLICY_CLASSES.illegalContent); - return Object.freeze({ - classes: [...classes], - blockedClasses: [...classes].filter((policyClass) => BLOCKED_PUBLIC_PROVIDER_CLASSES.has(policyClass)), - publicProviderSafe: [...classes].every((policyClass) => !BLOCKED_PUBLIC_PROVIDER_CLASSES.has(policyClass)) - }); -} - -export function validatePooldayPolicyClasses(request = {}) { - const reasons = []; - const classification = classifyPooldayPrompt(request.prompt || ''); - const explicitTags = Array.isArray(request.policyTags) ? request.policyTags.map(String) : []; - const blockedTags = explicitTags.filter((tag) => BLOCKED_PUBLIC_PROVIDER_CLASSES.has(tag)); - if (classification.blockedClasses.length > 0) { - reasons.push(`prompt policy classes are not allowed for public browser providers: ${classification.blockedClasses.join(', ')}`); - } - if (blockedTags.length > 0) { - reasons.push(`policyTags are not allowed for public browser providers: ${blockedTags.join(', ')}`); - } - return { - ok: reasons.length === 0, - reasons, - classification: { - ...classification, - explicitTags - } - }; -} - export function validateDeterministicGenerationConfig(config = {}) { - const reasons = []; - const allowedKeys = new Set(Object.keys(DETERMINISTIC_GENERATION_CONFIG)); - for (const [key, expected] of Object.entries(DETERMINISTIC_GENERATION_CONFIG)) { - if (config[key] !== expected) reasons.push(`generationConfig.${key} must be ${expected}`); - } - for (const key of Object.keys(config || {})) { - if (!allowedKeys.has(key)) reasons.push(`generationConfig.${key} is not allowed`); - } - return reasons; + return validateGenerationConfig(config, DETERMINISTIC_GENERATION_CONFIG); } export function validatePolicyRequest(request = {}) { @@ -100,6 +48,13 @@ export function validatePolicyRequest(request = {}) { if (!request.modelRequirements?.manifestHash) reasons.push('modelRequirements.manifestHash is required'); if (!request.modelRequirements?.runtime) reasons.push('modelRequirements.runtime is required'); if (!request.modelRequirements?.backend) reasons.push('modelRequirements.backend is required'); + if ( + policy + && request.modelRequirements?.modelId + && !policy.allowedModels?.includes(request.modelRequirements.modelId) + ) { + reasons.push(`model ${request.modelRequirements.modelId} is not allowed by policy ${policyId}`); + } if (policy) reasons.push(...validateDeterministicGenerationConfig(request.generationConfig || {})); if (policy) reasons.push(...validateLaunchModelRequirement(request.modelRequirements || {}).reasons); if (request.prompt !== undefined || request.policyTags !== undefined) { diff --git a/self/pool/policy-validation.js b/self/pool/policy-validation.js new file mode 100644 index 000000000..12f147dea --- /dev/null +++ b/self/pool/policy-validation.js @@ -0,0 +1,68 @@ +/** + * @fileoverview Runtime-neutral Poolday request policy validation. + */ + +export const POOLDAY_POLICY_CLASSES = Object.freeze({ + publicText: 'public_text', + codeHelp: 'code_help', + benchmarkEval: 'benchmark_eval', + pii: 'pii', + secrets: 'secrets', + medicalPrivate: 'medical_private', + illegalContent: 'illegal_content' +}); + +const BLOCKED_PUBLIC_PROVIDER_CLASSES = new Set([ + POOLDAY_POLICY_CLASSES.pii, + POOLDAY_POLICY_CLASSES.secrets, + POOLDAY_POLICY_CLASSES.medicalPrivate, + POOLDAY_POLICY_CLASSES.illegalContent +]); + +export function classifyPooldayPrompt(prompt = '') { + const text = String(prompt || ''); + const classes = new Set([POOLDAY_POLICY_CLASSES.publicText]); + if (/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/i.test(text)) classes.add(POOLDAY_POLICY_CLASSES.pii); + if (/\b(api[_-]?key|secret|password|private[_-]?key|token)\b\s*[:=]/i.test(text)) classes.add(POOLDAY_POLICY_CLASSES.secrets); + if (/\b(sk-[a-z0-9]{12,}|AIza[0-9A-Za-z_-]{20,})\b/.test(text)) classes.add(POOLDAY_POLICY_CLASSES.secrets); + if (/\b(patient|diagnosis|medical record|prescription)\b/i.test(text)) classes.add(POOLDAY_POLICY_CLASSES.medicalPrivate); + if (/\b(malware|credential theft|phishing kit|exploit chain)\b/i.test(text)) classes.add(POOLDAY_POLICY_CLASSES.illegalContent); + return Object.freeze({ + classes: [...classes], + blockedClasses: [...classes].filter((policyClass) => BLOCKED_PUBLIC_PROVIDER_CLASSES.has(policyClass)), + publicProviderSafe: [...classes].every((policyClass) => !BLOCKED_PUBLIC_PROVIDER_CLASSES.has(policyClass)) + }); +} + +export function validatePooldayPolicyClasses(request = {}) { + const reasons = []; + const classification = classifyPooldayPrompt(request.prompt || ''); + const explicitTags = Array.isArray(request.policyTags) ? request.policyTags.map(String) : []; + const blockedTags = explicitTags.filter((tag) => BLOCKED_PUBLIC_PROVIDER_CLASSES.has(tag)); + if (classification.blockedClasses.length > 0) { + reasons.push(`prompt policy classes are not allowed for public browser providers: ${classification.blockedClasses.join(', ')}`); + } + if (blockedTags.length > 0) { + reasons.push(`policyTags are not allowed for public browser providers: ${blockedTags.join(', ')}`); + } + return { + ok: reasons.length === 0, + reasons, + classification: { + ...classification, + explicitTags + } + }; +} + +export function validateGenerationConfig(config = {}, expectedConfig = {}) { + const reasons = []; + const allowedKeys = new Set(Object.keys(expectedConfig)); + for (const [key, expected] of Object.entries(expectedConfig)) { + if (config[key] !== expected) reasons.push(`generationConfig.${key} must be ${expected}`); + } + for (const key of Object.keys(config || {})) { + if (!allowedKeys.has(key)) reasons.push(`generationConfig.${key} is not allowed`); + } + return reasons; +} diff --git a/self/pool/pool-config.json b/self/pool/pool-config.json index 52981d949..9a3b52bf4 100644 --- a/self/pool/pool-config.json +++ b/self/pool/pool-config.json @@ -1,6 +1,6 @@ { "schema": "reploid.pool.config/v1", - "configVersion": "2026-07-24.doppler-0.5.1.v1", + "configVersion": "2026-07-27.doppler-0.5.1.v1", "claim": "receipt-backed, audit-backed, reputation-backed, policy-controlled browser inference", "forbiddenClaims": [ "trustless", @@ -373,7 +373,8 @@ "gemma-3-270m-it-q4k-ehf16-af32", "qwen-3-5-0-8b-q4k-ehaf16", "qwen-3-embedding-0-6b-q4k-ehf16-af32", - "gemma-4-e2b-it-q4k-ehf16-af32-int4ple" + "gemma-4-e2b-it-q4k-ehf16-af32-int4ple", + "esm2-t12-35m-ur50d-f32-af32" ], "minProviderReputation": 0, "maxQueueDepth": 100, @@ -404,7 +405,8 @@ "gemma-3-270m-it-q4k-ehf16-af32", "qwen-3-5-0-8b-q4k-ehaf16", "qwen-3-embedding-0-6b-q4k-ehf16-af32", - "gemma-4-e2b-it-q4k-ehf16-af32-int4ple" + "gemma-4-e2b-it-q4k-ehf16-af32-int4ple", + "esm2-t12-35m-ur50d-f32-af32" ], "minProviderReputation": 0, "maxQueueDepth": 100, @@ -437,7 +439,8 @@ "gemma-3-270m-it-q4k-ehf16-af32", "qwen-3-5-0-8b-q4k-ehaf16", "qwen-3-embedding-0-6b-q4k-ehf16-af32", - "gemma-4-e2b-it-q4k-ehf16-af32-int4ple" + "gemma-4-e2b-it-q4k-ehf16-af32-int4ple", + "esm2-t12-35m-ur50d-f32-af32" ], "minProviderReputation": 0, "maxQueueDepth": 100, @@ -472,7 +475,8 @@ "gemma-3-270m-it-q4k-ehf16-af32", "qwen-3-5-0-8b-q4k-ehaf16", "qwen-3-embedding-0-6b-q4k-ehf16-af32", - "gemma-4-e2b-it-q4k-ehf16-af32-int4ple" + "gemma-4-e2b-it-q4k-ehf16-af32-int4ple", + "esm2-t12-35m-ur50d-f32-af32" ], "minProviderReputation": 0, "maxQueueDepth": 100, diff --git a/self/pool/shard-negotiation.js b/self/pool/shard-negotiation.js deleted file mode 100644 index 620e07efa..000000000 --- a/self/pool/shard-negotiation.js +++ /dev/null @@ -1,188 +0,0 @@ -/** - * @fileoverview Descriptor hash negotiation handshake for Reploid orchestration. - * Section 2 of TODO_REPLOID.md: Descriptor Hash Negotiation Handshake. - * - * Three-step protocol: - * 1. Coordinator sends Negotiate(manifestHash, shardHashes) - * 2. Peer responds NegotiationResponse(HAS_SHARDS | FETCH_FAIL) - * 3. Coordinator sends Dispatch or Terminate - */ - -export const SHARD_NEGOTIATION_VERSION = 'reploid_shard_negotiation/v1'; - -export const NEGOTIATION_STATES = Object.freeze({ - INIT: 'INIT', - NEGOTIATE_SENT: 'NEGOTIATE_SENT', - HAS_SHARDS: 'HAS_SHARDS', - FETCH_FAIL: 'FETCH_FAIL', - DISPATCHED: 'DISPATCHED', - TERMINATED: 'TERMINATED', - TIMEOUT: 'TIMEOUT' -}); - -export const NEGOTIATION_RESPONSE_TYPES = Object.freeze({ - HAS_SHARDS: 'HAS_SHARDS', - FETCH_FAIL: 'FETCH_FAIL' -}); - -const DEFAULT_TIMEOUT_MS = 500; - -function buildNegotiateMessage({ coordinatorId, peerId, manifestHash, shardHashes, sessionId }) { - if (!manifestHash) throw new TypeError('manifestHash is required'); - if (!Array.isArray(shardHashes) || !shardHashes.length) throw new TypeError('shardHashes must be a non-empty array'); - return Object.freeze({ - negotiationVersion: SHARD_NEGOTIATION_VERSION, - type: 'NEGOTIATE', - coordinatorId: String(coordinatorId || ''), - peerId: String(peerId || ''), - sessionId: String(sessionId || ''), - manifestHash: String(manifestHash), - shardHashes: Object.freeze([...shardHashes].map(String)), - sentAt: new Date().toISOString() - }); -} - -function buildDispatchMessage({ coordinatorId, peerId, sessionId, assignmentId }) { - return Object.freeze({ - negotiationVersion: SHARD_NEGOTIATION_VERSION, - type: 'DISPATCH', - coordinatorId: String(coordinatorId || ''), - peerId: String(peerId || ''), - sessionId: String(sessionId || ''), - assignmentId: String(assignmentId || ''), - sentAt: new Date().toISOString() - }); -} - -function buildTerminateMessage({ coordinatorId, peerId, sessionId, reason }) { - return Object.freeze({ - negotiationVersion: SHARD_NEGOTIATION_VERSION, - type: 'TERMINATE', - coordinatorId: String(coordinatorId || ''), - peerId: String(peerId || ''), - sessionId: String(sessionId || ''), - reason: String(reason || 'FETCH_FAIL'), - sentAt: new Date().toISOString() - }); -} - -function buildNegotiationResponse({ peerId, sessionId, type, missingShards = [] }) { - if (!NEGOTIATION_RESPONSE_TYPES[type]) throw new TypeError(`type must be HAS_SHARDS or FETCH_FAIL`); - return Object.freeze({ - negotiationVersion: SHARD_NEGOTIATION_VERSION, - type, - peerId: String(peerId || ''), - sessionId: String(sessionId || ''), - missingShards: Object.freeze([...missingShards].map(String)), - respondedAt: new Date().toISOString() - }); -} - -export async function runCoordinatorNegotiation({ - coordinatorId, - peerId, - sessionId, - assignmentId, - manifestHash, - shardHashes, - sendToPeer, - waitForResponse, - timeoutMs = DEFAULT_TIMEOUT_MS -}) { - let state = NEGOTIATION_STATES.INIT; - const log = []; - - const record = (event) => log.push({ ...event, at: new Date().toISOString() }); - - const negotiateMsg = buildNegotiateMessage({ coordinatorId, peerId, manifestHash, shardHashes, sessionId }); - await sendToPeer(negotiateMsg); - state = NEGOTIATION_STATES.NEGOTIATE_SENT; - record({ step: 1, type: 'NEGOTIATE_SENT', manifestHash, shardCount: shardHashes.length }); - - let response; - try { - response = await Promise.race([ - waitForResponse(), - new Promise((_, reject) => setTimeout(() => reject(new Error('negotiation_timeout')), timeoutMs)) - ]); - } catch (err) { - state = NEGOTIATION_STATES.TIMEOUT; - record({ step: 2, type: 'TIMEOUT', reason: err.message }); - const terminate = buildTerminateMessage({ coordinatorId, peerId, sessionId, reason: 'TIMEOUT' }); - await sendToPeer(terminate).catch(() => {}); - return { state, log, peerId, outcome: 'TIMEOUT' }; - } - - if (response.type === NEGOTIATION_RESPONSE_TYPES.HAS_SHARDS) { - state = NEGOTIATION_STATES.HAS_SHARDS; - record({ step: 2, type: 'HAS_SHARDS' }); - const dispatch = buildDispatchMessage({ coordinatorId, peerId, sessionId, assignmentId }); - await sendToPeer(dispatch); - state = NEGOTIATION_STATES.DISPATCHED; - record({ step: 3, type: 'DISPATCHED', assignmentId }); - return { state, log, peerId, outcome: 'DISPATCHED' }; - } - - state = NEGOTIATION_STATES.FETCH_FAIL; - record({ step: 2, type: 'FETCH_FAIL', missingShards: response.missingShards || [] }); - const terminate = buildTerminateMessage({ coordinatorId, peerId, sessionId, reason: 'FETCH_FAIL' }); - await sendToPeer(terminate); - state = NEGOTIATION_STATES.TERMINATED; - record({ step: 3, type: 'TERMINATED', reason: 'FETCH_FAIL' }); - return { state, log, peerId, outcome: 'FETCH_FAIL' }; -} - -export async function runPeerNegotiation({ - peerId, - sessionId, - waitForNegotiate, - checkLocalShards, - fetchMissingShards = null, - sendToCoordinator -}) { - const negotiateMsg = await waitForNegotiate(); - const { manifestHash, shardHashes = [] } = negotiateMsg; - - const locallyPresent = await checkLocalShards({ manifestHash, shardHashes }); - const missing = shardHashes.filter((h) => !locallyPresent.includes(h)); - - if (missing.length && typeof fetchMissingShards === 'function') { - try { - await fetchMissingShards({ manifestHash, missing }); - const verified = await checkLocalShards({ manifestHash, shardHashes }); - const stillMissing = shardHashes.filter((h) => !verified.includes(h)); - if (stillMissing.length) { - await sendToCoordinator(buildNegotiationResponse({ peerId, sessionId, type: 'FETCH_FAIL', missingShards: stillMissing })); - return { outcome: 'FETCH_FAIL', missing: stillMissing }; - } - } catch { - await sendToCoordinator(buildNegotiationResponse({ peerId, sessionId, type: 'FETCH_FAIL', missingShards: missing })); - return { outcome: 'FETCH_FAIL', missing }; - } - } else if (missing.length) { - await sendToCoordinator(buildNegotiationResponse({ peerId, sessionId, type: 'FETCH_FAIL', missingShards: missing })); - return { outcome: 'FETCH_FAIL', missing }; - } - - await sendToCoordinator(buildNegotiationResponse({ peerId, sessionId, type: 'HAS_SHARDS' })); - return { outcome: 'HAS_SHARDS', manifestHash }; -} - -export { - buildNegotiateMessage, - buildNegotiationResponse, - buildDispatchMessage, - buildTerminateMessage -}; - -export default { - SHARD_NEGOTIATION_VERSION, - NEGOTIATION_STATES, - NEGOTIATION_RESPONSE_TYPES, - buildNegotiateMessage, - buildNegotiationResponse, - buildDispatchMessage, - buildTerminateMessage, - runCoordinatorNegotiation, - runPeerNegotiation -}; diff --git a/self/styles/landing-mono.css b/self/styles/landing-mono.css deleted file mode 100644 index cae10b142..000000000 --- a/self/styles/landing-mono.css +++ /dev/null @@ -1,284 +0,0 @@ -/* Landing Page - Layout & Animation Only - Uses rd.css for all primitives */ - -/* === LANDING LAYOUT === */ -body { - min-height: 100vh; - display: flex; - align-items: center; - justify-content: center; -} - -.landing { - display: flex; - flex-direction: column; - align-items: center; - gap: 4rem; - padding: 2rem; -} - -/* === ORBITAL RINGS === */ -.orbit-container { - position: relative; - width: 320px; - height: 320px; - cursor: pointer; -} - -/* Circular track borders */ -.orbit-container::before, -.orbit-container::after { - content: ""; - position: absolute; - left: 50%; - top: 50%; - transform: translate(-50%, -50%); - border-radius: 50%; - pointer-events: none; -} - -/* Inner track (REPLOID) - solid circle */ -.orbit-container::before { - width: 150px; - height: 150px; - border: var(--border-sm) solid var(--fg); - opacity: var(--opacity-disabled); -} - -/* Outer track (DOPPLER) - dashed circle */ -.orbit-container::after { - width: 260px; - height: 260px; - border: var(--border-sm) dashed var(--fg); - opacity: var(--opacity-disabled); -} - -.ring { - position: absolute; - inset: 0; - font-size: 28px; - font-weight: 700; -} - -.ring span { - position: absolute; - left: 50%; - top: 50%; - margin-left: -0.5em; - margin-top: -0.5em; - width: 1em; - text-align: center; - transition: transform 0.3s, opacity 0.3s; -} - -/* Inner ring letters (REPLOID) */ -.ring.inner span { - color: var(--fg); - opacity: 0.4; -} - -/* Outer ring letters (DOPPLER) */ -.ring.outer span { - color: var(--fg); - opacity: 0.25; -} - -/* Orbital radius tokens */ -.orbit-container { - --orbit-radius-inner: 75px; - --orbit-radius-outer: 130px; -} - -/* Position letters in a circle */ -.ring.inner span { --r: var(--orbit-radius-inner); /* inner orbital ring radius */ } -.ring.outer span { --r: var(--orbit-radius-outer); /* outer orbital ring radius */ } - -.ring span:nth-child(1) { transform: rotate(0deg) translateY(calc(var(--r) * -1)) rotate(0deg); } -.ring span:nth-child(2) { transform: rotate(51.4deg) translateY(calc(var(--r) * -1)) rotate(-51.4deg); } -.ring span:nth-child(3) { transform: rotate(102.8deg) translateY(calc(var(--r) * -1)) rotate(-102.8deg); } -.ring span:nth-child(4) { transform: rotate(154.3deg) translateY(calc(var(--r) * -1)) rotate(-154.3deg); } -.ring span:nth-child(5) { transform: rotate(205.7deg) translateY(calc(var(--r) * -1)) rotate(-205.7deg); } -.ring span:nth-child(6) { transform: rotate(257.1deg) translateY(calc(var(--r) * -1)) rotate(-257.1deg); } -.ring span:nth-child(7) { transform: rotate(308.6deg) translateY(calc(var(--r) * -1)) rotate(-308.6deg); } - -/* Ring animations */ -.ring.inner { animation: spin-cw 30s linear infinite; } -.ring.outer { animation: spin-ccw 30s linear infinite; } - -.orbit-container.paused .ring { animation-play-state: paused; } - -.orbit-container.fast .ring.inner, -.orbit-container.fast .ring.outer { animation-duration: 10s; } - -/* Letter hover */ -.ring span:hover { - transform: rotate(var(--rot)) translateY(calc(var(--r) * -1)) rotate(calc(var(--rot) * -1)) scale(1.2); - z-index: 10; - opacity: 1 !important; -} - -/* Container hover */ -.orbit-container:hover .ring.inner span { opacity: var(--opacity-secondary); } -.orbit-container:hover .ring.outer span { opacity: 0.4; } - -/* Link letters (D in REPLOID, R in DOPPLER) */ -.ring span.link { - text-decoration: underline; - text-underline-offset: 4px; -} - -/* Bisymmetry: hovering link letters */ -.orbit-container.bisymmetry .ring span.link { - opacity: 1 !important; - transform: rotate(var(--rot)) translateY(calc(var(--r) * -1)) rotate(calc(var(--rot) * -1)) scale(1.3); -} - -/* Entanglement: project hover affects orbital */ -.orbit-container.entangle-reploid .ring.inner { animation-duration: 15s; } -.orbit-container.entangle-reploid .ring.inner span { opacity: 0.8; } -.orbit-container.entangle-doppler .ring.outer { animation-duration: 15s; } -.orbit-container.entangle-doppler .ring.outer span { opacity: var(--opacity-secondary); } - -/* Letter crossing effects */ -.ring span.crossing { - transform: rotate(var(--rot)) translateY(calc(var(--r) * -1)) rotate(calc(var(--rot) * -1)) scale(1.25) !important; - opacity: 0.8 !important; -} - -.orbit-container.vertex-crossing .ring span.link { - transform: rotate(var(--rot)) translateY(calc(var(--r) * -1)) rotate(calc(var(--rot) * -1)) scale(1.4) !important; - opacity: 1 !important; -} - -@keyframes spin-cw { to { transform: rotate(360deg); } } -@keyframes spin-ccw { to { transform: rotate(-360deg); } } - -/* Ripples */ -.ripple { - position: absolute; - left: 50%; - top: 50%; - width: 0; - height: 0; - border-radius: 50%; - transform: translate(-50%, -50%); - animation: ripple 4s ease-out infinite; - pointer-events: none; -} - -.ripple.r1 { border: var(--border-sm) solid var(--fg); } -.ripple.r2 { border: var(--border-sm) dashed var(--fg); animation-delay: 1.3s; } -.ripple.r3 { border: var(--border-sm) dotted var(--fg); animation-delay: 2.6s; } - -@keyframes ripple { - 0% { width: 0; height: 0; opacity: var(--opacity-disabled); } - 100% { width: 400px; height: 400px; opacity: 0; } -} - -/* === PROJECT LINKS === */ -.projects { - display: flex; - flex-direction: column; - gap: 1.5rem; - align-items: center; -} - -.project { - text-decoration: none; - color: var(--fg); - transition: transform 0.3s ease; -} - -.project:hover { transform: scale(1.02); } - -/* Recursive box effect */ -.box-recursive { - position: relative; - padding: 4px; -} - -.box-recursive::before { - content: ""; - position: absolute; - inset: 0; - border: var(--border-sm) solid var(--fg); - opacity: 0.1; - transition: opacity 0.3s; -} - -.box-recursive::after { - content: ""; - position: absolute; - inset: var(--space-sm); - border: var(--border-sm) solid var(--fg); - opacity: var(--opacity-disabled); - transition: opacity 0.3s; -} - -.box-inner { - position: relative; - padding: 1.5rem 2rem; - border: var(--border-sm) solid var(--fg); - background: var(--bg); - width: 340px; - text-align: center; - opacity: var(--opacity-muted); - transition: opacity 0.3s, border-width 0.3s; -} - -/* REPLOID - solid borders */ -.project.reploid .box-recursive::before, -.project.reploid .box-recursive::after, -.project.reploid .box-inner { border-style: solid; } - -/* DOPPLER - dashed borders */ -.project.doppler .box-recursive::before, -.project.doppler .box-recursive::after, -.project.doppler .box-inner { border-style: dashed; } - -/* Hover states */ -.project:hover .box-recursive::before { opacity: 0.25; } -.project:hover .box-recursive::after { opacity: 0.4; } -.project:hover .box-inner { opacity: 1; border-width: var(--border-md); } - -/* Labels */ -.project .label { - font-size: 1.6rem; - font-weight: 700; - display: block; - margin-bottom: var(--space-sm); - letter-spacing: 0.1em; -} - -.project .desc { - font-size: 0.9rem; - display: block; - opacity: var(--opacity-secondary); -} - -.cycle { - font-size: 1.5rem; - opacity: 0.2; - transition: opacity 0.3s; -} - -.projects:hover .cycle { opacity: var(--opacity-muted); } - -/* === RESPONSIVE === */ -@media (max-width: 700px) { - .orbit-container { width: 260px; height: 260px; } - .orbit-container::before { width: 120px; height: 120px; } - .orbit-container::after { width: 210px; height: 210px; } - .ring { font-size: 22px; } - .orbit-container { --orbit-radius-inner: 60px; --orbit-radius-outer: 105px; } - - @keyframes ripple { - 0% { width: 0; height: 0; opacity: var(--opacity-disabled); } - 100% { width: 320px; height: 320px; opacity: 0; } - } - - .project .label { font-size: 1.4rem; } - .box-inner { padding: 1rem 1.5rem; width: 280px; opacity: var(--opacity-ghost); } - .box-recursive::before { opacity: 0.2; } - .box-recursive::after { opacity: 0.25; } -} diff --git a/self/styles/proto/hitl.css b/self/styles/proto/hitl.css deleted file mode 100644 index 677269780..000000000 --- a/self/styles/proto/hitl.css +++ /dev/null @@ -1,283 +0,0 @@ -/* HITL Widget - rd.css compliant - Human-in-the-loop approval UI */ - -.hitl-widget { - border: var(--border-sm) solid var(--fg); - padding: var(--space-md); - font-size: 11px; -} - -.hitl-widget.hitl-active { - border-left: var(--border-lg) dashed var(--fg); -} - -.hitl-widget.hitl-auto { - border-left: var(--border-lg) solid var(--fg); -} - -.hitl-widget.hitl-every-n { - border-left: var(--border-lg) dotted var(--fg); -} - -.hitl-header { - display: flex; - align-items: center; - gap: var(--space-sm); - margin-bottom: var(--space-sm); -} - -.hitl-icon { - font-size: 16px; -} - -.hitl-title { - flex: 1; - font-weight: 500; -} - -.hitl-toggle { - background: var(--bg); - border: var(--border-sm) solid var(--fg); - padding: 4px var(--space-sm); - cursor: pointer; - font-size: 11px; - color: var(--fg); -} - -.hitl-toggle:hover { - background: var(--fg); - color: var(--bg); -} - -.hitl-mode-select { - flex: 1; - background: var(--bg); - border: var(--border-sm) solid var(--fg); - padding: 4px var(--space-sm); - color: var(--fg); - font-size: 11px; - cursor: pointer; -} - -.hitl-mode-select:hover { - background: var(--fg); - color: var(--bg); -} - -.hitl-config { - margin: var(--space-sm) 0; - border: var(--border-sm) dashed var(--fg); - padding: var(--space-sm); -} - -.hitl-config-label { - display: flex; - align-items: center; - gap: 6px; - font-size: 11px; -} - -.hitl-steps-input { - width: 50px; - background: var(--bg); - border: var(--border-sm) solid var(--fg); - padding: 2px 6px; - color: var(--fg); - font-size: 11px; -} - -.hitl-step-counter { - opacity: var(--opacity-secondary); - font-size: 11px; -} - -.hitl-queue { - margin: var(--space-sm) 0; - border: var(--border-sm) dashed var(--fg); - padding: var(--space-sm); -} - -.hitl-queue-header { - font-weight: 500; - margin-bottom: var(--space-sm); -} - -.hitl-item { - display: flex; - justify-content: space-between; - align-items: center; - padding: 6px 0; - border-bottom: var(--border-sm) dotted var(--fg); -} - -.hitl-item:last-child { - border-bottom: none; -} - -.hitl-item-info { - display: flex; - flex-direction: column; - gap: 2px; -} - -.hitl-item-module { - font-weight: 500; - font-size: 11px; -} - -.hitl-item-action { - opacity: var(--opacity-ghost); - font-size: 11px; -} - -.hitl-item-actions { - display: flex; - gap: 4px; -} - -.hitl-approve, -.hitl-reject { - width: 24px; - height: 24px; - border: var(--border-sm) solid var(--fg); - cursor: pointer; - font-size: 11px; - background: var(--bg); - color: var(--fg); -} - -.hitl-approve:hover, -.hitl-reject:hover { - background: var(--fg); - color: var(--bg); -} - -.hitl-approve { - border-width: var(--border-md); -} - -.hitl-reject { - border-style: dashed; -} - -.hitl-more { - text-align: center; - opacity: var(--opacity-secondary); - font-size: 11px; - padding-top: 4px; -} - -.hitl-stats { - display: flex; - gap: var(--space-md); - justify-content: center; -} - -.hitl-stat { - padding: 2px var(--space-sm); - font-size: 11px; - border: var(--border-sm) solid var(--fg); -} - -.hitl-stat.approved { - border-width: var(--border-md); -} - -.hitl-stat.rejected { - border-style: dashed; -} - -.hitl-stat.auto { - border-style: dotted; -} - -.hitl-disabled { - opacity: var(--opacity-muted); - text-align: center; - padding: 20px; -} - -/* Mobile responsive styles for HITL widget */ -@media (max-width: 768px) { - .hitl-widget { - padding: var(--space-sm); - } - - .hitl-header { - flex-wrap: wrap; - gap: 6px; - } - - .hitl-item { - flex-direction: column; - align-items: flex-start; - gap: 6px; - } - - .hitl-item-actions { - width: 100%; - justify-content: flex-end; - } - - .hitl-stats { - flex-wrap: wrap; - gap: var(--space-sm); - } -} - -@media (max-width: 480px) { - .hitl-widget { - padding: var(--space-sm); - font-size: 11px; - } - - .hitl-icon { - font-size: 14px; - } - - .hitl-title { - font-size: 12px; - } - - .hitl-mode-select { - padding: 6px; - font-size: 11px; - } - - .hitl-approve, - .hitl-reject { - width: 32px; - height: 32px; - } - - .hitl-stat { - font-size: 10px; - padding: 2px 6px; - } - - .hitl-config { - padding: 6px; - } - - .hitl-queue { - padding: 6px; - } -} - -/* Touch optimization for HITL */ -@media (pointer: coarse) { - .hitl-approve, - .hitl-reject { - min-width: 44px; - min-height: 44px; - } - - .hitl-toggle { - min-height: 36px; - padding: 6px var(--space-md); - } - - .hitl-mode-select { - min-height: 40px; - } -} diff --git a/self/styles/proto/index.css b/self/styles/proto/index.css index 81f6a7216..f3c37c74e 100644 --- a/self/styles/proto/index.css +++ b/self/styles/proto/index.css @@ -8,6 +8,5 @@ @import url('./history.css'); @import url('./components.css'); @import url('./inline-chat.css'); -@import url('./hitl.css'); @import url('./optimization.css'); @import url('./responsive.css'); diff --git a/self/styles/proto/panels.css b/self/styles/proto/panels.css index e2db6948a..6d6562d2b 100644 --- a/self/styles/proto/panels.css +++ b/self/styles/proto/panels.css @@ -127,106 +127,6 @@ border-width: var(--border-md); } -/* Schema Registry Panel */ -.schema-panel { - display: flex; - flex-direction: column; - gap: var(--space-md); - height: 100%; -} - -.schema-header { - display: flex; - justify-content: space-between; - align-items: center; - flex-wrap: wrap; - gap: var(--space-sm); -} - -.schema-controls { - display: flex; - gap: var(--space-sm); - align-items: center; -} - -.schema-search { - width: 240px; - padding: var(--space-sm); - background: var(--bg); - color: var(--fg); - border: var(--border-sm) solid var(--fg); - font-family: var(--font-a); - font-size: 11px; -} - -.schema-search:focus { - border-width: var(--border-md); - outline: none; -} - -.schema-columns { - flex: 1; - display: grid; - grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); - gap: var(--space-md); -} - -.schema-card { - background: var(--bg); - border: var(--border-sm) solid var(--fg); - padding: var(--space-md); - font-size: 11px; -} - -.schema-card header { - display: flex; - justify-content: space-between; - align-items: flex-start; - gap: var(--space-md); - margin-bottom: var(--space-sm); -} - -.schema-card pre { - margin: 0; - background: var(--bg); - border: var(--border-sm) dotted var(--fg); - padding: var(--space-sm); - font-size: 11px; -} - -.schema-badge { - font-size: 11px; - border: var(--border-sm) solid var(--fg); - padding: 1px 6px; - text-transform: uppercase; -} - -.schema-worker-meta { - display: flex; - gap: var(--space-md); - flex-wrap: wrap; - font-size: 11px; - margin-bottom: 6px; -} - -.schema-meta-label { - opacity: var(--opacity-muted); - margin-right: 4px; -} - -.schema-tools { - display: flex; - flex-wrap: wrap; - gap: 6px; - font-size: 11px; -} - -.schema-tools code { - background: var(--bg); - border: var(--border-sm) solid var(--fg); - padding: 2px 4px; -} - /* Status Panel */ .status-panel { display: flex; @@ -877,144 +777,6 @@ word-break: break-word; } -/* Arena Results Panel */ -.arena-panel { - display: flex; - flex-direction: column; - gap: var(--space-md); - height: 100%; -} - -.arena-panel-header { - display: flex; - justify-content: space-between; - align-items: center; - flex-wrap: wrap; - gap: var(--space-sm); -} - -.arena-title { - display: flex; - align-items: baseline; - gap: var(--space-sm); -} - -.arena-count { - font-size: 11px; - opacity: var(--opacity-muted); -} - -.arena-controls { - display: flex; - gap: var(--space-sm); -} - -.arena-list { - flex: 1; - display: flex; - flex-direction: column; - gap: var(--space-md); -} - -.arena-entry { - border: var(--border-sm) solid var(--fg); - background: var(--bg); - padding: var(--space-md); - display: flex; - flex-direction: column; - gap: var(--space-md); -} - -.arena-entry-header { - display: flex; - justify-content: space-between; - align-items: flex-start; - gap: var(--space-md); -} - -.arena-entry-title { - font-size: 12px; - font-weight: 600; -} - -.arena-entry-meta { - font-size: 11px; - opacity: var(--opacity-muted); - margin-top: 4px; -} - -.arena-entry-actions { - display: flex; - gap: var(--space-sm); -} - -.arena-section { - display: flex; - flex-direction: column; - gap: var(--space-sm); -} - -.arena-section-title { - font-size: 11px; - text-transform: uppercase; - letter-spacing: 0.08em; - opacity: var(--opacity-muted); -} - -.arena-score-grid { - display: grid; - gap: 6px; - font-size: 11px; -} - -.arena-score-row { - display: grid; - grid-template-columns: 1.6fr 0.7fr 0.7fr 0.7fr; - gap: var(--space-sm); - align-items: center; - padding: 6px var(--space-sm); - background: var(--bg); - border: var(--border-sm) solid var(--fg); -} - -.arena-score-row.winner { - border-width: var(--border-md); -} - -.arena-score-row.arena-score-header { - background: transparent; - border: none; - padding: 0 var(--space-sm); - opacity: var(--opacity-muted); - text-transform: uppercase; - letter-spacing: 0.06em; -} - -.arena-diff { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); - gap: var(--space-md); -} - -.arena-diff-column { - background: var(--bg); - border: var(--border-sm) solid var(--fg); - padding: var(--space-sm); -} - -.arena-diff-header { - font-size: 11px; - opacity: var(--opacity-muted); - margin-bottom: 6px; -} - -.arena-diff-column pre { - margin: 0; - font-family: var(--font-a); - font-size: 11px; - white-space: pre-wrap; -} - /* Tools Panel */ .tools-panel { display: flex; diff --git a/self/styles/proto/responsive.css b/self/styles/proto/responsive.css index 973a26b1f..08b69b51f 100644 --- a/self/styles/proto/responsive.css +++ b/self/styles/proto/responsive.css @@ -311,15 +311,6 @@ gap: 2px; } - /* Schema panel */ - .schema-columns { - grid-template-columns: 1fr; - } - - .schema-search { - width: 100%; - } - /* Telemetry panel */ .telemetry-header { flex-direction: column; @@ -359,9 +350,7 @@ } .tool-summary-main, - .memory-entry summary, - .arena-entry-header, - .arena-entry-actions { + .memory-entry summary { align-items: flex-start; flex-direction: column; } diff --git a/self/styles/vfs-explorer.css b/self/styles/vfs-explorer.css deleted file mode 100644 index aabda590a..000000000 --- a/self/styles/vfs-explorer.css +++ /dev/null @@ -1,454 +0,0 @@ -/* VFS Explorer Styles - rd.css compliant - Uses only --fg, --bg, --prism and rd.css patterns */ - -.vfs-explorer { - display: flex; - flex-direction: column; - height: 100%; - background: var(--bg); - border: var(--border-md) solid var(--fg); - overflow: hidden; -} - -.vfs-toolbar { - display: flex; - gap: var(--space-sm); - padding: var(--space-sm); - border-bottom: var(--border-sm) solid var(--fg); -} - -.vfs-search { - flex: 1; - padding: var(--space-sm); - background: var(--bg); - border: var(--border-sm) solid var(--fg); - color: var(--fg); - font-family: var(--font-a); - font-size: 13px; -} - -.vfs-search:focus { - outline: none; - border-width: var(--border-md); -} - -.vfs-search:placeholder-shown { - border-style: dotted; -} - -.vfs-search::placeholder { - color: var(--fg); - opacity: var(--opacity-muted); -} - -.vfs-toolbar button { - padding: var(--space-sm); - background: var(--bg); - border: var(--border-sm) solid var(--fg); - color: var(--fg); - cursor: pointer; - font-size: 16px; -} - -.vfs-toolbar button:hover { - background: var(--fg); - color: var(--bg); -} - -.vfs-tree { - flex: 1; - overflow-y: auto; - padding: 4px; - font-family: var(--font-a); - font-size: 13px; -} - -/* Use rd.css scrollbar styling (inherited from ::-webkit-scrollbar) */ - -.vfs-item { - display: flex; - align-items: center; - gap: var(--space-sm); - padding: 4px var(--space-sm); - cursor: pointer; - user-select: none; - white-space: nowrap; -} - -.vfs-item:hover { - background: var(--fg); - color: var(--bg); -} - -.vfs-item.selected { - background: var(--fg); - color: var(--bg); - border-left: var(--border-lg) solid var(--fg); -} - -.vfs-item.highlight .vfs-name { - border: var(--border-sm) dashed var(--fg); - padding: 2px 4px; -} - -.vfs-folder-header { - font-weight: 600; -} - -.vfs-file { - color: var(--fg); -} - -.vfs-expand { - display: inline-block; - width: 16px; - text-align: center; - font-size: 10px; - opacity: var(--opacity-ghost); -} - -.vfs-icon { - font-size: 14px; - min-width: 20px; -} - -.vfs-name { - flex: 1; - overflow: hidden; - text-overflow: ellipsis; -} - -.vfs-count { - font-size: 11px; - opacity: var(--opacity-secondary); - margin-left: 4px; -} - -.vfs-size { - font-size: 11px; - opacity: var(--opacity-secondary); - margin-left: auto; -} - -.vfs-children.collapsed { - display: none; -} - -.vfs-children.expanded { - display: block; -} - -.vfs-stats { - padding: var(--space-sm); - text-align: center; - font-size: 11px; - opacity: var(--opacity-secondary); - border-top: var(--border-sm) solid var(--fg); -} - -/* File Viewer Modal */ - -.vfs-file-viewer-modal { - position: fixed; - inset: 0; - z-index: 10000; - display: none; - align-items: center; - justify-content: center; - background: var(--bg); -} - -.vfs-file-viewer-overlay { - position: absolute; - inset: 0; - background: var(--bg); -} - -.vfs-file-viewer-content { - position: relative; - display: flex; - flex-direction: column; - width: 90%; - max-width: 1200px; - height: 80vh; - background: var(--bg); - border: var(--border-lg) solid var(--fg); -} - -.vfs-file-viewer-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: var(--space-sm) var(--space-md); - border-bottom: var(--border-sm) solid var(--fg); -} - -.vfs-file-viewer-title { - display: flex; - align-items: center; - gap: var(--space-sm); - font-family: var(--font-a); - font-size: 14px; - font-weight: 600; -} - -.vfs-file-viewer-close { - padding: 4px var(--space-sm); - background: var(--bg); - border: var(--border-sm) solid var(--fg); - color: var(--fg); - font-size: 18px; - cursor: pointer; -} - -.vfs-file-viewer-close:hover { - background: var(--fg); - color: var(--bg); -} - -.vfs-file-viewer-meta { - padding: var(--space-sm) var(--space-md); - font-size: 12px; - opacity: var(--opacity-ghost); - border-bottom: var(--border-sm) dotted var(--fg); -} - -.vfs-file-viewer-body { - flex: 1; - overflow: auto; - padding: var(--space-md); -} - -.vfs-file-viewer-body pre { - margin: 0; - font-family: var(--font-a); - font-size: 13px; - line-height: 1.6; - color: var(--fg); -} - -.vfs-file-viewer-body code { - display: block; - white-space: pre; - color: var(--fg); -} - -.vfs-file-viewer-footer { - display: flex; - gap: var(--space-sm); - padding: var(--space-sm) var(--space-md); - border-top: var(--border-sm) solid var(--fg); -} - -.vfs-file-viewer-footer button { - padding: var(--space-sm) var(--space-md); - background: var(--bg); - border: var(--border-sm) solid var(--fg); - color: var(--fg); - font-size: 13px; - cursor: pointer; -} - -.vfs-file-viewer-footer button:hover { - background: var(--fg); - color: var(--bg); -} - -/* ======================================== - RESPONSIVE DESIGN - ======================================== */ - -/* Tablet breakpoint */ -@media (max-width: 1024px) { - .vfs-toolbar { - gap: var(--space-sm); - padding: var(--space-sm); - } - - .vfs-item { - padding: 3px var(--space-sm); - font-size: 12px; - } - - .vfs-file-viewer-content { - width: 92%; - height: 85vh; - } -} - -/* Mobile breakpoint */ -@media (max-width: 768px) { - .vfs-explorer { - font-size: 12px; - } - - .vfs-toolbar { - flex-wrap: wrap; - padding: var(--space-sm); - } - - .vfs-search { - min-width: 100%; - flex-basis: 100%; - order: 1; - font-size: 12px; - } - - .vfs-toolbar button { - padding: var(--space-sm); - font-size: 14px; - } - - .vfs-tree { - font-size: 11px; - } - - .vfs-item { - padding: 4px var(--space-sm); - gap: 4px; - } - - .vfs-icon { - font-size: 12px; - min-width: 18px; - } - - .vfs-size, - .vfs-count { - font-size: 10px; - } - - .vfs-stats { - padding: var(--space-sm); - font-size: 10px; - } - - /* File viewer modal */ - .vfs-file-viewer-content { - width: 95%; - height: 90vh; - } - - .vfs-file-viewer-header { - padding: var(--space-sm); - } - - .vfs-file-viewer-title { - font-size: 12px; - gap: var(--space-sm); - } - - .vfs-file-viewer-meta { - padding: var(--space-sm); - font-size: 11px; - } - - .vfs-file-viewer-body { - padding: var(--space-sm); - } - - .vfs-file-viewer-body pre { - font-size: 11px; - line-height: 1.5; - } - - .vfs-file-viewer-footer { - padding: var(--space-sm); - gap: var(--space-sm); - flex-wrap: wrap; - } - - .vfs-file-viewer-footer button { - padding: var(--space-sm); - font-size: 12px; - flex: 1; - min-width: 80px; - } -} - -/* Small mobile breakpoint */ -@media (max-width: 480px) { - .vfs-toolbar button { - padding: 5px var(--space-sm); - font-size: 12px; - } - - .vfs-item { - padding: 3px 4px; - font-size: 10px; - } - - .vfs-name { - overflow: hidden; - text-overflow: ellipsis; - max-width: 150px; - } - - .vfs-file-viewer-content { - width: 98%; - height: 95vh; - } - - .vfs-file-viewer-title { - font-size: 11px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - - .vfs-file-viewer-body { - padding: var(--space-sm); - } - - .vfs-file-viewer-body pre { - font-size: 10px; - line-height: 1.4; - } - - .vfs-file-viewer-footer button { - font-size: 11px; - padding: 5px var(--space-sm); - } -} - -/* Touch optimizations */ -@media (hover: none) and (pointer: coarse) { - .vfs-item { - min-height: 36px; - align-items: center; - } - - .vfs-toolbar button { - min-height: 36px; - min-width: 36px; - } - - .vfs-file-viewer-close { - min-height: 40px; - min-width: 40px; - } - - .vfs-file-viewer-footer button { - min-height: 40px; - } - - /* Better scroll */ - .vfs-tree { - -webkit-overflow-scrolling: touch; - } - - .vfs-file-viewer-body { - -webkit-overflow-scrolling: touch; - } -} - -/* Landscape mobile */ -@media (max-height: 500px) and (orientation: landscape) { - .vfs-file-viewer-content { - height: 95vh; - } - - .vfs-file-viewer-body pre { - font-size: 10px; - } -} diff --git a/self/testing/arena/doppler-integration.js b/self/testing/arena/doppler-integration.js deleted file mode 100644 index 4a87a0508..000000000 --- a/self/testing/arena/doppler-integration.js +++ /dev/null @@ -1,535 +0,0 @@ -/** - * @fileoverview Doppler-Arena Integration - * Wires Doppler inference with LoRA adapters to ArenaHarness for - * competitive expert pool evaluation. - * - * Key Features: - * - Connect adapter loading to ArenaHarness expert pools - * - Enable arena competitions with different LoRA adapters - * - Measure passRate with adapter switching - * - Support adapter composition (merge strategies) - */ - -const DopplerArenaIntegration = { - metadata: { - id: 'DopplerArenaIntegration', - version: '1.0.0', - dependencies: ['ArenaHarness', 'Utils', 'EventBus'], - optional: ['LLMClient'], - type: 'testing' - }, - - factory: (deps) => { - const { ArenaHarness, Utils, EventBus, LLMClient } = deps; - const { logger, generateId } = Utils; - - // Doppler provider reference (lazy loaded) - let _dopplerProvider = null; - let _baseModelId = null; - let _adapterCache = new Map(); - - /** - * Initialize Doppler provider - */ - const initDoppler = async () => { - if (_dopplerProvider?.getCapabilities?.()?.initialized) { - return _dopplerProvider; - } - - try { - // Try dynamic import for Doppler - const { DopplerProvider } = await import('@simulatte/doppler/provider'); - _dopplerProvider = DopplerProvider; - - if (!_dopplerProvider.getCapabilities().initialized) { - await _dopplerProvider.init(); - } - - if (!_dopplerProvider.getCapabilities().available) { - throw new Error('Doppler not available - WebGPU may not be supported'); - } - - logger.info('[DopplerArena] Doppler initialized'); - return _dopplerProvider; - } catch (err) { - logger.warn('[DopplerArena] Doppler not available, using LLMClient fallback'); - return null; - } - }; - - /** - * Load base model for adapter competitions - */ - const loadBaseModel = async (modelId, modelUrl = null, options = {}) => { - const provider = await initDoppler(); - if (!provider) { - throw new Error('Doppler provider not available'); - } - - const caps = provider.getCapabilities(); - if (caps.currentModelId !== modelId) { - logger.info(`[DopplerArena] Loading base model: ${modelId}`); - await provider.loadModel(modelId, modelUrl, options.onProgress); - } - - _baseModelId = modelId; - return true; - }; - - /** - * Load and cache a LoRA adapter - */ - const loadAdapter = async (adapterId, adapterManifest) => { - const provider = await initDoppler(); - if (!provider) { - throw new Error('Doppler provider not available'); - } - - // Cache adapter for quick switching - _adapterCache.set(adapterId, adapterManifest); - - await provider.loadLoRAAdapter(adapterManifest); - logger.info(`[DopplerArena] Loaded adapter: ${adapterId}`); - - return adapterId; - }; - - /** - * Switch to a different adapter (hot-swap) - */ - const switchAdapter = async (adapterId) => { - const provider = await initDoppler(); - if (!provider) { - throw new Error('Doppler provider not available'); - } - - if (!_adapterCache.has(adapterId) && adapterId !== null) { - throw new Error(`Adapter not loaded: ${adapterId}`); - } - - // null means no adapter (base model only) - if (adapterId === null) { - await provider.unloadLoRAAdapter(); - logger.info('[DopplerArena] Switched to base model (no adapter)'); - } else { - const manifest = _adapterCache.get(adapterId); - await provider.loadLoRAAdapter(manifest); - logger.info(`[DopplerArena] Switched to adapter: ${adapterId}`); - } - - return adapterId; - }; - - /** - * Run inference with current adapter - */ - const runInference = async (prompt, options = {}) => { - const provider = await initDoppler(); - if (!provider) { - // Fallback to LLMClient if available - if (LLMClient) { - return LLMClient.chat( - [{ role: 'user', content: prompt }], - { provider: 'doppler', maxTokens: options.maxTokens || 256 } - ); - } - throw new Error('No inference provider available'); - } - - const messages = [ - { role: 'system', content: options.systemPrompt || 'You are a helpful assistant.' }, - { role: 'user', content: prompt } - ]; - - const startTime = performance.now(); - const result = await provider.chat(messages, { - maxTokens: options.maxTokens || 256, - temperature: options.temperature || 0.7, - topP: options.topP, - topK: options.topK, - }); - const durationMs = performance.now() - startTime; - - return { - content: result.content, - durationMs, - tokensGenerated: result.usage?.completionTokens || 0, - tokPerSec: result.usage?.completionTokens / (durationMs / 1000) || 0, - adapter: provider.getActiveLoRA?.() || null, - }; - }; - - /** - * Create expert configuration for arena competition - */ - const createExpert = (adapterId, options = {}) => { - return { - id: adapterId || 'base-model', - adapter: adapterId, - name: options.name || adapterId || 'Base Model', - modelId: _baseModelId, - weight: options.weight || 1.0, - temperature: options.temperature, - maxTokens: options.maxTokens, - }; - }; - - /** - * Run arena expert pool competition with adapters - * - * @param {Object} task - Task configuration - * @param {string} task.prompt - The prompt to evaluate - * @param {Object} task.schema - Optional JSON schema for output validation - * @param {number} task.maxTokens - Max tokens per response - * @param {Array} experts - Array of expert configs (each has adapterId) - * @param {Object} options - Additional options - * @returns {Object} Competition results with winner and rankings - */ - const runAdapterCompetition = async (task, experts, options = {}) => { - if (!experts || experts.length === 0) { - throw new Error('At least one expert required'); - } - - const runId = generateId('arena-adapter'); - logger.info(`[DopplerArena] Starting adapter competition: ${experts.length} experts`); - - EventBus.emit('arena:adapter:start', { - runId, - expertCount: experts.length, - task: task.prompt?.slice(0, 100), - }); - - const results = []; - - for (const expert of experts) { - const expertResult = { - expert, - output: null, - score: { score: 0, valid: true, errors: [] }, - durationMs: 0, - tokPerSec: 0, - }; - - try { - // Switch to this expert's adapter - await switchAdapter(expert.adapter); - - // Run inference - const startTime = performance.now(); - const inferenceResult = await runInference(task.prompt, { - maxTokens: task.maxTokens || expert.maxTokens || 256, - temperature: task.temperature || expert.temperature || 0.7, - systemPrompt: task.systemPrompt, - }); - - expertResult.output = inferenceResult.content; - expertResult.durationMs = inferenceResult.durationMs; - expertResult.tokPerSec = inferenceResult.tokPerSec; - - // Score the output - expertResult.score = ArenaHarness.scoreOutput - ? ArenaHarness.scoreOutput(inferenceResult.content, task, options) - : { score: 0.5, valid: true, errors: [] }; - - logger.info(`[DopplerArena] Expert ${expert.id}: score=${expertResult.score.score.toFixed(2)}, ${expertResult.tokPerSec.toFixed(1)} tok/s`); - } catch (err) { - expertResult.score = { score: 0, valid: false, errors: [err.message] }; - logger.error(`[DopplerArena] Expert ${expert.id} failed: ${err.message}`); - } - - results.push(expertResult); - } - - // Sort by score (descending) - results.sort((a, b) => b.score.score - a.score.score); - - const winner = results[0]; - const summary = { - runId, - totalExperts: experts.length, - passedExperts: results.filter(r => r.score.valid).length, - winnerExpert: winner.expert.id, - winnerScore: winner.score.score, - winnerTokPerSec: winner.tokPerSec, - passRate: (results.filter(r => r.score.valid && r.score.score > 0.5).length / experts.length) * 100, - }; - - EventBus.emit('arena:adapter:complete', { - runId, - summary, - winner: winner.expert.id, - }); - - return { - winner, - results, - summary, - }; - }; - - /** - * Merge multiple LoRA adapters using different strategies - * - * @param {Array} adapters - Array of { id, manifest, weight } - * @param {string} strategy - 'add', 'lerp', 'ties', 'dare' - * @returns {Object} Merged adapter manifest - */ - const mergeAdapters = (adapters, strategy = 'lerp') => { - if (adapters.length === 0) { - throw new Error('At least one adapter required for merge'); - } - - if (adapters.length === 1) { - return adapters[0].manifest; - } - - logger.info(`[DopplerArena] Merging ${adapters.length} adapters with strategy: ${strategy}`); - - // Validate all adapters have same structure - const first = adapters[0].manifest; - const rank = first.rank; - const alpha = first.alpha; - - for (const { manifest } of adapters) { - if (manifest.rank !== rank) { - throw new Error('All adapters must have same rank for merging'); - } - } - - // Merge tensors based on strategy - const mergedTensors = []; - const tensorsByName = new Map(); - - // Group tensors by name - for (const { manifest, weight } of adapters) { - for (const tensor of manifest.tensors || []) { - if (!tensorsByName.has(tensor.name)) { - tensorsByName.set(tensor.name, []); - } - tensorsByName.get(tensor.name).push({ tensor, weight }); - } - } - - // Merge each tensor group - for (const [name, tensors] of tensorsByName) { - const shape = tensors[0].tensor.shape; - const totalElements = shape[0] * shape[1]; - - // Get data arrays - const dataArrays = tensors.map(({ tensor, weight }) => { - let data; - if (tensor.data) { - data = new Float32Array(tensor.data); - } else if (tensor.base64) { - // Decode base64 - const binary = atob(tensor.base64); - const bytes = new Uint8Array(binary.length); - for (let i = 0; i < binary.length; i++) { - bytes[i] = binary.charCodeAt(i); - } - data = new Float32Array(bytes.buffer); - } else { - throw new Error(`Tensor ${name} missing data for merge`); - } - return { data, weight }; - }); - - // Apply merge strategy - const merged = new Float32Array(totalElements); - - switch (strategy) { - case 'add': - // Simple addition: sum all weighted tensors - for (const { data, weight } of dataArrays) { - for (let i = 0; i < totalElements; i++) { - merged[i] += data[i] * weight; - } - } - break; - - case 'lerp': - // Linear interpolation (normalized weights) - const totalWeight = dataArrays.reduce((sum, { weight }) => sum + weight, 0); - for (const { data, weight } of dataArrays) { - const normalizedWeight = weight / totalWeight; - for (let i = 0; i < totalElements; i++) { - merged[i] += data[i] * normalizedWeight; - } - } - break; - - case 'ties': - // TIES merging: trim, elect, sign, merge - // Simplified version: keep values where majority agree on sign - for (let i = 0; i < totalElements; i++) { - let positiveCount = 0; - let negativeCount = 0; - let positiveSum = 0; - let negativeSum = 0; - - for (const { data, weight } of dataArrays) { - if (data[i] > 0) { - positiveCount++; - positiveSum += data[i] * weight; - } else if (data[i] < 0) { - negativeCount++; - negativeSum += data[i] * weight; - } - } - - if (positiveCount > negativeCount) { - merged[i] = positiveSum / positiveCount; - } else if (negativeCount > positiveCount) { - merged[i] = negativeSum / negativeCount; - } - // else: zero (disagreement) - } - break; - - case 'dare': - // DARE: Drop And REscale - randomly drop some values - const dropRate = 0.1; // 10% drop rate - const rescale = 1 / (1 - dropRate); - - for (const { data, weight } of dataArrays) { - for (let i = 0; i < totalElements; i++) { - if (Math.random() > dropRate) { - merged[i] += data[i] * weight * rescale; - } - } - } - - // Normalize - const dareTotal = dataArrays.reduce((sum, { weight }) => sum + weight, 0); - for (let i = 0; i < totalElements; i++) { - merged[i] /= dareTotal; - } - break; - - default: - throw new Error(`Unknown merge strategy: ${strategy}`); - } - - mergedTensors.push({ - name, - shape, - dtype: 'f32', - data: Array.from(merged), - }); - } - - return { - name: `merged-${strategy}-${adapters.length}adapters`, - version: '1.0.0', - baseModel: first.baseModel, - rank, - alpha, - targetModules: first.targetModules, - tensors: mergedTensors, - }; - }; - - /** - * Run A/B test between adapters - */ - const runABTest = async (task, adapterA, adapterB, options = {}) => { - const numTrials = options.trials || 5; - const results = { a: [], b: [] }; - - for (let i = 0; i < numTrials; i++) { - // Randomize order to avoid position bias - const aFirst = Math.random() > 0.5; - const first = aFirst ? adapterA : adapterB; - const second = aFirst ? adapterB : adapterA; - - // Run first - await switchAdapter(first); - const firstResult = await runInference(task.prompt, task); - - // Run second - await switchAdapter(second); - const secondResult = await runInference(task.prompt, task); - - // Record results - if (aFirst) { - results.a.push(firstResult); - results.b.push(secondResult); - } else { - results.b.push(firstResult); - results.a.push(secondResult); - } - } - - // Compute statistics - const avgA = results.a.reduce((sum, r) => sum + r.tokPerSec, 0) / numTrials; - const avgB = results.b.reduce((sum, r) => sum + r.tokPerSec, 0) / numTrials; - - return { - adapterA: { - id: adapterA, - avgTokPerSec: avgA, - results: results.a, - }, - adapterB: { - id: adapterB, - avgTokPerSec: avgB, - results: results.b, - }, - winner: avgA > avgB ? adapterA : adapterB, - speedupPercent: ((Math.max(avgA, avgB) - Math.min(avgA, avgB)) / Math.min(avgA, avgB)) * 100, - }; - }; - - /** - * Get current adapter status - */ - const getStatus = async () => { - const provider = await initDoppler().catch(() => null); - if (!provider) { - return { - available: false, - baseModel: null, - activeAdapter: null, - cachedAdapters: [], - }; - } - - return { - available: true, - baseModel: _baseModelId, - activeAdapter: provider.getActiveLoRA?.() || null, - cachedAdapters: Array.from(_adapterCache.keys()), - capabilities: provider.getCapabilities(), - }; - }; - - /** - * Clean up resources - */ - const cleanup = async () => { - _adapterCache.clear(); - if (_dopplerProvider?.destroy) { - await _dopplerProvider.destroy(); - } - _dopplerProvider = null; - _baseModelId = null; - logger.info('[DopplerArena] Cleaned up'); - }; - - return { - initDoppler, - loadBaseModel, - loadAdapter, - switchAdapter, - runInference, - createExpert, - runAdapterCompetition, - mergeAdapters, - runABTest, - getStatus, - cleanup, - }; - } -}; - -export default DopplerArenaIntegration; diff --git a/self/testing/arena/index.js b/self/testing/arena/index.js deleted file mode 100644 index 8f6bd8b56..000000000 --- a/self/testing/arena/index.js +++ /dev/null @@ -1,10 +0,0 @@ -/** - * @fileoverview Arena Test Harness - Module exports - * Internal testing library for REPLOID model comparison and self-modification gating. - */ - -export { default as VFSSandbox } from './vfs-sandbox.js'; -export { default as ArenaCompetitor } from './competitor.js'; -export { default as ArenaMetrics } from './arena-metrics.js'; -export { default as ArenaHarness } from './arena-harness.js'; -export { default as DopplerArenaIntegration } from './doppler-integration.js'; diff --git a/self/ui/UI.js b/self/ui/UI.js deleted file mode 100644 index 7537fe3e2..000000000 --- a/self/ui/UI.js +++ /dev/null @@ -1,7 +0,0 @@ -/** - * @fileoverview UI - Legacy entrypoint alias - * - * Some agents assume /ui/UI.js exists. Keep this shim for compatibility. - */ - -export { default } from './proto.js'; diff --git a/self/ui/boot-wizard/steps/detect.js b/self/ui/boot-wizard/steps/detect.js deleted file mode 100644 index e1d46575e..000000000 --- a/self/ui/boot-wizard/steps/detect.js +++ /dev/null @@ -1,146 +0,0 @@ -/** - * @fileoverview Detect step renderers - */ - -/** - * Render START step (resume saved config) - */ -export function renderStartStep(state) { - const saved = state.savedConfig; - - if (!saved) return ''; - - return ` -
-

REPLOID

-

self-modifying AI agent in the browser

-
-
- Provider - ${saved.primaryProvider || 'Unknown'} -
-
- Model - ${saved.primaryModel || 'Unknown'} -
-
- Key - ${saved.hasSavedKey ? 'Saved locally' : 'Not saved'} -
-
- - ${saved.hasSavedKey ? ` -
- - -
- ` : ` -
- - - -
-
- -
- `} -
- `; -} - -/** - * Render DETECT step - unified intro/landing page - */ -export function renderDetectStep(state) { - const { detection, savedConfig } = state; - const isScanning = detection.scanning; - - // If not scanning yet, show intro/landing - if (!isScanning && !detection.webgpu.checked) { - return ` -
-

REPLOID

-

self-modifying AI agent in the browser

- -
- ${savedConfig ? ` - ${!savedConfig.hasSavedKey ? ` - - ` : ''} - - - ` : ` - - `} -
-
- `; - } - - // Scanning in progress - return ` -
-

Scanning

- -
-
- ${detection.webgpu.checked ? (detection.webgpu.supported ? '★' : '☒') : '☍'} - WebGPU - - ${detection.webgpu.checked ? (detection.webgpu.supported ? 'Available' : 'Not supported') : '...'} - -
- -
- ${detection.doppler?.checked ? (detection.doppler?.supported ? '★' : '☒') : '☍'} - Doppler - - ${detection.doppler?.checked ? (detection.doppler?.supported ? 'Ready' : 'N/A') : '...'} - -
- -
- ${detection.ollama?.checked ? (detection.ollama?.detected ? '★' : detection.ollama?.blocked ? '△' : '☒') : '☍'} - Ollama - - ${detection.ollama?.checked - ? (detection.ollama?.detected - ? `${detection.ollama.models?.length || 0} models` - : detection.ollama?.blocked ? 'Blocked' : 'N/A') - : '...'} - -
- -
- ${detection.proxy?.checked ? (detection.proxy?.detected ? '★' : detection.proxy?.blocked ? '△' : '☒') : '☍'} - Proxy - - ${detection.proxy?.checked - ? (detection.proxy?.detected ? 'Found' : detection.proxy?.blocked ? 'Blocked' : 'N/A') - : '...'} - -
-
- -
- -
-
- `; -} diff --git a/self/ui/boot-wizard/zero-function.js b/self/ui/boot-wizard/zero-function.js deleted file mode 100644 index 5d106dd75..000000000 --- a/self/ui/boot-wizard/zero-function.js +++ /dev/null @@ -1 +0,0 @@ -export * from '../../config/zero-inference.js'; diff --git a/self/ui/capsule/index.js b/self/ui/capsule/index.js deleted file mode 100644 index 0536e9072..000000000 --- a/self/ui/capsule/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from '../../capsule/index.js'; diff --git a/self/ui/components/arena-results.js b/self/ui/components/arena-results.js deleted file mode 100644 index 7ab4127a6..000000000 --- a/self/ui/components/arena-results.js +++ /dev/null @@ -1,286 +0,0 @@ -/** - * @fileoverview Arena Results - Competition history and score breakdown UI - */ - -const ArenaResults = { - metadata: { - id: 'ArenaResults', - version: '1.0.0', - dependencies: ['Utils', 'EventBus', 'ArenaHarness?'], - async: false, - type: 'ui' - }, - - factory: (deps) => { - const { Utils, EventBus, ArenaHarness } = deps; - const { logger, escapeHtml, generateId } = Utils; - - const MAX_HISTORY = 20; - const _history = []; - let _container = null; - let _subscriptions = []; - - const formatTime = (ts) => { - try { - return new Date(ts).toLocaleTimeString(); - } catch { - return ''; - } - }; - - const addEntry = (entry) => { - _history.unshift(entry); - if (_history.length > MAX_HISTORY) { - _history.pop(); - } - render(); - }; - - const toAgentEntry = (result) => { - const solutions = Array.isArray(result?.solutions) ? [...result.solutions] : []; - solutions.sort((a, b) => (b.score || 0) - (a.score || 0)); - return { - id: generateId('arena'), - source: 'agent', - cycle: result?.cycle || null, - mode: result?.mode || 'arena', - winner: result?.winner?.model || solutions[0]?.model || null, - solutions, - timestamp: Date.now() - }; - }; - - const toHarnessEntry = (payload) => { - const results = Array.isArray(payload?.results) ? payload.results : []; - return { - id: payload?.runId || generateId('arena'), - source: 'harness', - cycle: null, - mode: 'arena', - winner: payload?.winner || payload?.summary?.fastestPassing || null, - summary: payload?.summary || null, - results, - timestamp: Date.now() - }; - }; - - const getDiffPair = (entry) => { - if (entry?.solutions?.length >= 2) { - const winner = entry.solutions[0]; - const runnerUp = entry.solutions[1]; - return { - winnerLabel: winner?.model || 'winner', - winnerContent: winner?.code || winner?.content || '', - runnerLabel: runnerUp?.model || 'runner-up', - runnerContent: runnerUp?.code || runnerUp?.content || '' - }; - } - - if (entry?.results?.length >= 2) { - const withSolutions = entry.results.filter(r => r.solution); - if (withSolutions.length >= 2) { - const winner = withSolutions[0]; - const runnerUp = withSolutions[1]; - return { - winnerLabel: winner?.competitorName || 'winner', - winnerContent: winner?.solution || '', - runnerLabel: runnerUp?.competitorName || 'runner-up', - runnerContent: runnerUp?.solution || '' - }; - } - } - - return null; - }; - - const renderScoreBreakdown = (entry) => { - if (entry?.solutions?.length) { - return ` -
-
- Model - Score - Quality - Tokens -
- ${entry.solutions.map(sol => ` -
- ${escapeHtml(sol.model || 'unknown')} - ${(sol.score || 0).toFixed(2)} - ${Number.isFinite(sol.quality) ? sol.quality.toFixed(2) : 'n/a'} - ${Number.isFinite(sol.tokens) ? sol.tokens : 'n/a'} -
- `).join('')} -
- `; - } - - if (entry?.results?.length) { - return ` -
-
- Competitor - Status - Time - Tokens -
- ${entry.results.map(res => ` -
- ${escapeHtml(res.competitorName || 'unknown')} - ${escapeHtml(res.status || 'UNKNOWN')} - ${Number.isFinite(res.executionMs) ? `${res.executionMs}ms` : 'n/a'} - ${Number.isFinite(res.tokenCount) ? res.tokenCount : 'n/a'} -
- `).join('')} -
- `; - } - - return '
No score data available
'; - }; - - const renderDiff = (entry) => { - const pair = getDiffPair(entry); - if (!pair) { - return '
Diff unavailable for this run
'; - } - - return ` -
-
-
Winner: ${escapeHtml(pair.winnerLabel)}
-
${escapeHtml(pair.winnerContent)}
-
-
-
Runner-up: ${escapeHtml(pair.runnerLabel)}
-
${escapeHtml(pair.runnerContent)}
-
-
- `; - }; - - const renderEntry = (entry, index) => { - const canRerun = entry.source === 'harness' && index === 0 && ArenaHarness?.rerunLast; - const winnerLabel = entry.winner ? escapeHtml(entry.winner) : 'unknown'; - - return ` -
-
-
-
Run ${entry.cycle ? `#${entry.cycle}` : 'summary'} (${escapeHtml(entry.mode)})
- -
-
- -
-
-
-
-
Score Breakdown
- ${renderScoreBreakdown(entry)} -
-
-
Winner and Runner-up
- ${renderDiff(entry)} -
-
-
- `; - }; - - const render = () => { - if (!_container) return; - const count = _history.length; - - _container.innerHTML = ` -
-
- Arena Results - ${count} run${count === 1 ? '' : 's'} -
-
- - -
-
-
- ${count === 0 - ? '
No arena runs yet
' - : _history.map(renderEntry).join('')} -
- `; - }; - - const handleAction = async (event) => { - const actionBtn = event.target.closest('[data-action]'); - if (!actionBtn) return; - const action = actionBtn.dataset.action; - const entryId = actionBtn.dataset.entryId; - const entry = _history.find(item => item.id === entryId); - - if (action === 'refresh') { - render(); - return; - } - - if (action === 'clear') { - _history.length = 0; - render(); - return; - } - - if (action === 'rerun') { - if (entry?.source === 'harness' && ArenaHarness?.rerunLast) { - try { - await ArenaHarness.rerunLast(); - } catch (err) { - logger.warn('[ArenaResults] Re-run failed', err.message); - } - } else { - EventBus.emit('arena:rerun-requested', { entryId, source: entry?.source || 'unknown' }); - } - } - }; - - const init = (containerId) => { - _container = typeof containerId === 'string' - ? document.getElementById(containerId) - : containerId; - - if (!_container) { - logger.warn('[ArenaResults] Container not found'); - return false; - } - - _subscriptions.push(EventBus.on('agent:arena-result', (result) => { - addEntry(toAgentEntry(result)); - }, 'ArenaResults')); - - _subscriptions.push(EventBus.on('arena:complete', (payload) => { - addEntry(toHarnessEntry(payload)); - }, 'ArenaResults')); - - _container.addEventListener('click', handleAction); - - render(); - logger.info('[ArenaResults] Initialized'); - return true; - }; - - const cleanup = () => { - _subscriptions.forEach(unsub => { - if (typeof unsub === 'function') unsub(); - }); - _subscriptions = []; - if (_container) { - _container.removeEventListener('click', handleAction); - } - }; - - return { init, cleanup }; - } -}; - -export default ArenaResults; diff --git a/self/ui/components/confirmation-modal.js b/self/ui/components/confirmation-modal.js deleted file mode 100644 index f382ebaab..000000000 --- a/self/ui/components/confirmation-modal.js +++ /dev/null @@ -1,102 +0,0 @@ -// Confirmation Modal Component for REPLOID - -const ConfirmationModal = { - metadata: { - id: 'ConfirmationModal', - version: '1.0.0', - dependencies: ['Utils'], - async: false, - type: 'ui' - }, - - factory: (deps) => { - const { Utils } = deps; - const { logger, escapeHtml } = Utils; - - let activeModal = null; - - const attachHandlers = (overlay, handlers) => { - overlay.querySelector('.modal-btn-confirm').addEventListener('click', handlers.confirm); - overlay.querySelector('.modal-btn-cancel').addEventListener('click', handlers.cancel); - overlay.querySelector('.modal-close').addEventListener('click', handlers.cancel); - document.addEventListener('keydown', handlers.escape); - overlay.addEventListener('click', handlers.overlayClick); - }; - - const confirm = (options = {}) => { - const { - title = 'Confirm Action', - message = 'Are you sure you want to proceed?', - confirmText = 'Confirm', - cancelText = 'Cancel', - danger = false, - details = null - } = options; - - return new Promise((resolve) => { - closeModal(); - - const overlay = document.createElement('div'); - overlay.className = 'modal-overlay'; - overlay.innerHTML = ` - - `; - - const handlers = { - confirm: () => { - closeModal(); - resolve(true); - }, - cancel: () => { - closeModal(); - resolve(false); - }, - escape: (event) => { - if (event.key === 'Escape') handlers.cancel(); - }, - overlayClick: (event) => { - if (event.target === overlay) handlers.cancel(); - } - }; - - document.body.appendChild(overlay); - attachHandlers(overlay, handlers); - - activeModal = { overlay, handlers }; - logger.info('[ConfirmationModal] Modal shown:', title); - }); - }; - - const closeModal = () => { - if (!activeModal) return; - const { overlay, handlers } = activeModal; - - document.removeEventListener('keydown', handlers.escape); - overlay.removeEventListener('click', handlers.overlayClick); - - if (overlay.parentNode) { - overlay.parentNode.removeChild(overlay); - } - - activeModal = null; - logger.info('[ConfirmationModal] Modal closed'); - }; - - return { confirm, closeModal }; - } -}; - -export default ConfirmationModal; diff --git a/self/ui/components/diff-viewer-ui.js b/self/ui/components/diff-viewer-ui.js deleted file mode 100644 index bc2191487..000000000 --- a/self/ui/components/diff-viewer-ui.js +++ /dev/null @@ -1,720 +0,0 @@ -// Interactive Diff Viewer UI Component for REPLOID Sentinel -// Provides rich diff visualization and interactive approval controls -// PX-3 Enhanced: Prism.js syntax highlighting + detailed statistics -// PHASE 3 UPDATE: Added Event-Driven Rollback capability - -const DiffViewerUI = { - metadata: { - id: 'DiffViewerUI', - version: '1.0.0', // Updated for DIUtils - description: 'Enhanced diff viewer with Prism.js highlighting, stats, and rollback events', - features: [ - 'Prism.js syntax highlighting for 10+ languages', - 'Side-by-side diff with color-coded changes', - 'Detailed per-file statistics (added/removed/modified lines)', - 'Language detection from file extensions', - 'Export to markdown, clipboard, and Web Share API', - 'Event-driven Rollback trigger' - ], - dependencies: ['Utils', 'StateManager', 'EventBus', 'ConfirmationModal?'], - externalDeps: ['Prism'], - async: false, - type: 'ui' - }, - - factory: (deps) => { - const { Utils, StateManager, EventBus, ConfirmationModal } = deps; - const { logger, escapeHtml } = Utils; - - let container = null; - let currentDiff = null; - - // Track event listeners for cleanup - const eventListeners = { - showDiff: null, - clearDiff: null - }; - - // Cleanup function to remove event listeners - const cleanup = () => { - if (eventListeners.showDiff) { - EventBus.off('diff:show', eventListeners.showDiff); - eventListeners.showDiff = null; - } - if (eventListeners.clearDiff) { - EventBus.off('diff:clear', eventListeners.clearDiff); - eventListeners.clearDiff = null; - } - }; - - // Initialize the diff viewer - const init = (containerId) => { - // Clean up any existing listeners first - cleanup(); - - container = document.getElementById(containerId); - if (!container) { - logger.error('[DiffViewerUI] Container not found:', containerId); - return; - } - - // Register event listeners and store references - eventListeners.showDiff = handleShowDiff; - eventListeners.clearDiff = clearDiff; - EventBus.on('diff:show', eventListeners.showDiff); - EventBus.on('diff:clear', eventListeners.clearDiff); - - logger.info('[DiffViewerUI] Initialized'); - }; - - // Handle showing a diff - const handleShowDiff = async (data) => { - const { dogs_path, session_id, turn } = data; - - try { - // Load and parse the dogs bundle - const dogsContent = await StateManager.getArtifactContent(dogs_path); - if (!dogsContent) { - showError('Dogs bundle not found'); - return; - } - - const changes = await parseDogsBundle(dogsContent); - currentDiff = { changes, dogs_path, session_id, turn }; - - renderDiff(changes); - - } catch (error) { - logger.error('[DiffViewerUI] Error showing diff:', error); - showError('Failed to load diff'); - } - }; - - // Parse dogs bundle using injected Utils parser - const parseDogsBundle = async (content) => { - // Use canonical parser from Utils - const baseChanges = Utils.parseDogsBundle(content); - - // Enrich with old content and UI state - const enrichedChanges = []; - for (const change of baseChanges) { - let oldContent = ''; - - // For MODIFY and DELETE operations, fetch current content - if (change.operation === 'MODIFY' || change.operation === 'DELETE') { - try { - oldContent = await StateManager.getArtifactContent(change.file_path) || ''; - } catch (err) { - console.error(`Failed to fetch old content for ${change.file_path}:`, err); - oldContent = '// Error loading original content'; - } - } - - enrichedChanges.push({ - ...change, - old_content: oldContent, - approved: true // Default to approved for smoother workflow - }); - } - - return enrichedChanges; - }; - - let actionClickHandler = null; - - // Render the diff viewer - const renderDiff = (changes) => { - if (!container) return; - - const html = ` -
-
-

Review Proposed Changes

-
- ${getChangeStats(changes)} -
-
- - - -
- ${changes.map((change, index) => renderFileChange(change, index)).join('')} -
- - -
- `; - - container.innerHTML = html; - bindDiffEvents(); - - // Initialize diff rendering for each file - changes.forEach((change, index) => { - if (change.operation === 'MODIFY') { - renderFileDiff(change, index); - } - }); - - // Setup scroll sync after a short delay to ensure DOM is ready - setTimeout(setupScrollSync, 100); - }; - - const bindDiffEvents = () => { - if (!container) return; - - if (actionClickHandler) { - container.removeEventListener('click', actionClickHandler); - } - - actionClickHandler = handleActionClick; - container.addEventListener('click', actionClickHandler); - - const diffFiles = container.querySelector('.diff-files'); - if (diffFiles) { - diffFiles.addEventListener('click', handleDiffFileClick); - diffFiles.addEventListener('change', handleApprovalChangeEvent); - } - }; - - const handleActionClick = (event) => { - const target = event.target.closest('[data-action]'); - if (!target) return; - const actionMap = { - 'approve-all': approveAll, - 'reject-all': rejectAll, - 'edit': editProposal, - 'copy': () => copyToClipboard(target), - 'export': exportMarkdown, - 'rollback': rollback, - 'cancel': cancel, - 'apply': applyApproved - }; - const handler = actionMap[target.dataset.action]; - if (handler) { - handler(); - } - }; - - const handleDiffFileClick = (event) => { - const expandBtn = event.target.closest('[data-expand]'); - if (!expandBtn) return; - const index = parseInt(expandBtn.dataset.expand, 10); - if (!Number.isNaN(index)) { - toggleExpand(index); - } - }; - - const handleApprovalChangeEvent = (event) => { - if (!event.target.classList.contains('approve-checkbox')) return; - const index = parseInt(event.target.dataset.index, 10); - if (!Number.isNaN(index)) { - toggleApproval(index, event.target.checked); - } - }; - - // Get change statistics - const getChangeStats = (changes) => { - const stats = { CREATE: 0, MODIFY: 0, DELETE: 0 }; - changes.forEach(c => stats[c.operation]++); - - return ` - +${stats.CREATE} new - ~${stats.MODIFY} modified - -${stats.DELETE} deleted - `; - }; - - // Render a single file change - const renderFileChange = (change, index) => { - const icon = { - CREATE: '☩', - MODIFY: '✎', - DELETE: '✄' - }[change.operation]; - - return ` -
-
-
- - ${change.file_path} - ${change.operation} -
-
- - -
-
- -
- `; - }; - - // Render the content of a change - const renderChangeContent = (change, index) => { - const language = detectLanguage(change.file_path); - - if (change.operation === 'CREATE') { - const highlightedCode = highlightCode(change.new_content, language); - const lines = change.new_content.split('\n').length; - return ` -
-
- +${lines} lines -
-
${highlightedCode}
-
- `; - } else if (change.operation === 'DELETE') { - const content = change.old_content || 'File will be deleted'; - const highlightedCode = change.old_content ? highlightCode(content, language) : content; - const lines = change.old_content ? change.old_content.split('\n').length : 0; - return ` -
-
- -${lines} lines -
-
${highlightedCode}
-
- `; - } else if (change.operation === 'MODIFY') { - return `
Loading diff...
`; - } - }; - - // Render a file diff for MODIFY operations - const renderFileDiff = async (change, index) => { - const container = document.getElementById(`diff-modify-${index}`); - if (!container) return; - - try { - const oldContent = await StateManager.getArtifactContent(change.file_path) || ''; - const newContent = change.new_content; - const diffHtml = generateSideBySideDiff(oldContent, newContent, change.file_path); - container.innerHTML = diffHtml; - } catch (error) { - container.innerHTML = '
Failed to load diff
'; - } - }; - - // Detect language from file path - const detectLanguage = (filePath) => { - const ext = filePath.split('.').pop().toLowerCase(); - const langMap = { - 'js': 'javascript', 'ts': 'typescript', 'json': 'json', 'css': 'css', 'html': 'markup', 'py': 'python', 'md': 'markdown' - }; - return langMap[ext] || 'javascript'; - }; - - // Apply syntax highlighting - const highlightCode = (code, language) => { - if (typeof Prism === 'undefined' || !Prism.languages[language]) { - return escapeHtml(code); - } - try { - return Prism.highlight(code, Prism.languages[language], language); - } catch (err) { - return escapeHtml(code); - } - }; - - // LCS-based diff algorithm for proper line matching - const computeLCS = (oldLines, newLines) => { - const m = oldLines.length; - const n = newLines.length; - - // Build LCS table - const dp = Array(m + 1).fill(null).map(() => Array(n + 1).fill(0)); - for (let i = 1; i <= m; i++) { - for (let j = 1; j <= n; j++) { - if (oldLines[i - 1] === newLines[j - 1]) { - dp[i][j] = dp[i - 1][j - 1] + 1; - } else { - dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); - } - } - } - - // Backtrack to find diff operations - const operations = []; - let i = m, j = n; - - while (i > 0 || j > 0) { - if (i > 0 && j > 0 && oldLines[i - 1] === newLines[j - 1]) { - operations.unshift({ type: 'equal', oldIdx: i - 1, newIdx: j - 1 }); - i--; j--; - } else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) { - operations.unshift({ type: 'add', newIdx: j - 1 }); - j--; - } else { - operations.unshift({ type: 'remove', oldIdx: i - 1 }); - i--; - } - } - - return operations; - }; - - // Calculate detailed diff statistics using LCS - const calculateDiffStats = (oldContent, newContent) => { - const oldLines = oldContent.split('\n'); - const newLines = newContent.split('\n'); - const ops = computeLCS(oldLines, newLines); - - let added = 0, removed = 0, unchanged = 0; - for (const op of ops) { - if (op.type === 'add') added++; - else if (op.type === 'remove') removed++; - else unchanged++; - } - - return { added, removed, modified: 0, unchanged, total: ops.length }; - }; - - // Generate side-by-side diff HTML using LCS - const generateSideBySideDiff = (oldContent, newContent, filePath = '') => { - const oldLines = oldContent.split('\n'); - const newLines = newContent.split('\n'); - const language = detectLanguage(filePath); - const ops = computeLCS(oldLines, newLines); - const stats = calculateDiffStats(oldContent, newContent); - - // Build aligned rows for side-by-side view - const rows = []; - for (const op of ops) { - if (op.type === 'equal') { - rows.push({ - oldLine: oldLines[op.oldIdx], - oldNum: op.oldIdx + 1, - newLine: newLines[op.newIdx], - newNum: op.newIdx + 1, - type: 'equal' - }); - } else if (op.type === 'remove') { - rows.push({ - oldLine: oldLines[op.oldIdx], - oldNum: op.oldIdx + 1, - newLine: null, - newNum: null, - type: 'remove' - }); - } else if (op.type === 'add') { - rows.push({ - oldLine: null, - oldNum: null, - newLine: newLines[op.newIdx], - newNum: op.newIdx + 1, - type: 'add' - }); - } - } - - // Generate HTML - const diffId = `diff-${Date.now()}`; - let html = '
'; - html += `+${stats.added}`; - html += `-${stats.removed}`; - html += '
'; - - html += `
`; - html += `
Original
`; - - for (const row of rows) { - const lineClass = row.type === 'remove' ? 'removed' : (row.type === 'add' ? 'empty' : ''); - if (row.oldLine !== null) { - html += `
${row.oldNum}${highlightCode(row.oldLine, language)}
`; - } else { - html += '
 
'; - } - } - html += '
'; - - html += `
Modified
`; - for (const row of rows) { - const lineClass = row.type === 'add' ? 'added' : (row.type === 'remove' ? 'empty' : ''); - if (row.newLine !== null) { - html += `
${row.newNum}${highlightCode(row.newLine, language)}
`; - } else { - html += '
 
'; - } - } - html += '
'; - - // Add scroll sync script - html += ``; - - return html; - }; - - // Setup scroll sync for diff panes (called after render) - const setupScrollSync = () => { - const diffs = container?.querySelectorAll('.side-by-side-diff'); - diffs?.forEach(diff => { - const panes = diff.querySelectorAll('.diff-lines'); - if (panes.length !== 2) return; - - let syncing = false; - panes.forEach(pane => { - pane.addEventListener('scroll', function() { - if (syncing) return; - syncing = true; - const scrollTop = this.scrollTop; - const scrollLeft = this.scrollLeft; - panes.forEach(p => { - if (p !== this) { - p.scrollTop = scrollTop; - p.scrollLeft = scrollLeft; - } - }); - requestAnimationFrame(() => { syncing = false; }); - }); - }); - }); - }; - - // Toggle file content expansion - const toggleExpand = (index) => { - const content = document.getElementById(`diff-content-${index}`); - if (content) { - const isExpanded = content.style.display !== 'none'; - content.style.display = isExpanded ? 'none' : 'block'; - } - }; - - // Toggle approval for a change - const toggleApproval = (index, state = null) => { - if (currentDiff && currentDiff.changes[index]) { - currentDiff.changes[index].approved = state === null - ? !currentDiff.changes[index].approved - : state; - updateApprovalStats(); - } - }; - - // Approve all changes - const approveAll = () => { - if (currentDiff) { - currentDiff.changes.forEach(c => c.approved = true); - document.querySelectorAll('.approve-checkbox').forEach(cb => cb.checked = true); - updateApprovalStats(); - } - }; - - // Reject all changes - const rejectAll = () => { - if (currentDiff) { - currentDiff.changes.forEach(c => c.approved = false); - document.querySelectorAll('.approve-checkbox').forEach(cb => cb.checked = false); - updateApprovalStats(); - } - }; - - // Update approval statistics - const updateApprovalStats = () => { - const approved = currentDiff.changes.filter(c => c.approved).length; - const total = currentDiff.changes.length; - const applyBtn = document.querySelector('.btn-apply'); - if (applyBtn) { - applyBtn.textContent = `Apply ${approved}/${total} Approved Changes`; - applyBtn.disabled = approved === 0; - } - }; - - // Apply approved changes - const applyApproved = async () => { - if (!currentDiff) return; - - const approvedChanges = currentDiff.changes.filter(c => c.approved); - if (approvedChanges.length === 0) { - showError('No changes approved'); - return; - } - - // Show confirmation dialog - const changeDetails = approvedChanges.map(c => `${c.operation}: ${c.file_path}`).join('\n'); - - const confirmed = ConfirmationModal - ? await ConfirmationModal.confirm({ - title: 'Apply Changes', - message: `Apply ${approvedChanges.length} approved change${approvedChanges.length > 1 ? 's' : ''}? This will modify your files.`, - confirmText: 'Apply Changes', - cancelText: 'Cancel', - danger: true, - details: changeDetails - }) - : confirm(`Apply ${approvedChanges.length} change(s)?\n\n${changeDetails}`); - - if (!confirmed) return; - - const filteredDogsPath = currentDiff.dogs_path.replace('.md', '-filtered.md'); - - EventBus.emit('proposal:approved', { - original_dogs_path: currentDiff.dogs_path, - filtered_dogs_path: filteredDogsPath, - approved_changes: approvedChanges, - session_id: currentDiff.session_id, - turn: currentDiff.turn - }); - - clearDiff(); - }; - - // edit the proposal - const editProposal = () => { - if (!currentDiff) return; - EventBus.emit('proposal:edit', { - dogs_path: currentDiff.dogs_path, - changes: currentDiff.changes - }); - }; - - // Trigger Rollback via EventBus (Decoupled from FSM) - const rollback = async () => { - const confirmed = ConfirmationModal - ? await ConfirmationModal.confirm({ - title: 'Emergency Rollback', - message: 'Are you sure you want to revert the file system to the state before these changes were proposed?', - confirmText: 'Rollback', - cancelText: 'Abort', - danger: true - }) - : confirm('Emergency Rollback: Revert file system?'); - - if (confirmed) { - logger.warn('[DiffViewerUI] Triggering manual rollback'); - EventBus.emit('proposal:rollback'); - clearDiff(); - } - }; - - // Cancel the diff viewer - const cancel = () => { - EventBus.emit('proposal:cancelled'); - clearDiff(); - }; - - // Clear the diff viewer - const clearDiff = () => { - if (container) container.innerHTML = ''; - currentDiff = null; - }; - - // Show an error message - const showError = (message) => { - if (container) { - container.innerHTML = `

☒ ${message}

`; - } - }; - - // Copy diff to clipboard - const copyToClipboard = async (btn) => { - if (!currentDiff) return; - try { - const markdown = generateDiffMarkdown(); - await navigator.clipboard.writeText(markdown); - if (btn) { - const originalText = btn.innerHTML; - btn.innerHTML = '✓ Copied!'; - setTimeout(() => { btn.innerHTML = originalText; }, 2000); - } - } catch (err) { - logger.error('[DiffViewerUI] Copy failed:', err); - } - }; - - // Generate diff summary markdown - const generateDiffMarkdown = () => { - if (!currentDiff) return ''; - const { changes, dogs_path } = currentDiff; - let md = `# Diff Summary\nSource: ${dogs_path}\n\n`; - changes.forEach((change, i) => { - md += `### ${i+1}. ${change.operation}: ${change.file_path}\n`; - }); - return md; - }; - - // Export as markdown file - const exportMarkdown = () => { - if (!currentDiff) return; - const markdown = generateDiffMarkdown(); - const blob = new Blob([markdown], { type: 'text/markdown' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = `diff-${Date.now()}.md`; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); - }; - - // Export public API - const publicApi = { - init, - showDiff: handleShowDiff, - clearDiff - }; - - return publicApi; - } -}; - -export default DiffViewerUI; diff --git a/self/ui/components/hitl-widget.js b/self/ui/components/hitl-widget.js deleted file mode 100644 index 5387731e9..000000000 --- a/self/ui/components/hitl-widget.js +++ /dev/null @@ -1,205 +0,0 @@ -/** - * @fileoverview HITL Widget - Approval queue UI component - * Displays pending approvals and allows approve/reject actions. - */ - -const HITLWidget = { - metadata: { - id: 'HITLWidget', - version: '1.0.0', - dependencies: ['Utils', 'EventBus', 'HITLController?'], - async: false, - type: 'ui' - }, - - factory: (deps) => { - const { Utils, EventBus, HITLController } = deps; - const { logger } = Utils; - - let _container = null; - let _updateHandler = null; - - const init = (containerId) => { - _container = typeof containerId === 'string' - ? document.getElementById(containerId) - : containerId; - - if (!_container) { - logger.warn('[HITLWidget] Container not found'); - return false; - } - - // Subscribe to HITL events for live updates - _updateHandler = () => render(); - EventBus.on('hitl:approval-pending', _updateHandler, 'HITLWidget'); - EventBus.on('hitl:approval-granted', _updateHandler, 'HITLWidget'); - EventBus.on('hitl:approval-rejected', _updateHandler, 'HITLWidget'); - EventBus.on('hitl:approval-mode-changed', _updateHandler, 'HITLWidget'); - EventBus.on('hitl:config-reset', _updateHandler, 'HITLWidget'); - - render(); - logger.info('[HITLWidget] Initialized'); - return true; - }; - - const cleanup = () => { - if (_updateHandler) { - EventBus.off('hitl:approval-pending', _updateHandler); - EventBus.off('hitl:approval-granted', _updateHandler); - EventBus.off('hitl:approval-rejected', _updateHandler); - EventBus.off('hitl:approval-mode-changed', _updateHandler); - EventBus.off('hitl:config-reset', _updateHandler); - _updateHandler = null; - } - }; - - const render = () => { - if (!_container) return; - - if (!HITLController) { - _container.innerHTML = '
HITL not available
'; - return; - } - - const state = HITLController.getState(); - const { config, approvalQueue, approvalStats } = state; - const mode = config.masterMode; - const isHITL = mode === 'hitl'; - const isEveryN = mode === 'every_n'; - const isAuto = mode === 'autonomous'; - - const modeIcon = isHITL ? '⚑' : (isEveryN ? '♺' : '☇'); - const modeTitle = isHITL ? 'HITL Mode' : (isEveryN ? `Every ${config.everyNSteps}` : 'Autonomous'); - const widgetClass = isHITL ? 'hitl-active' : (isEveryN ? 'hitl-every-n' : 'hitl-auto'); - - const html = ` -
-
- ${modeIcon} - -
- - ${isEveryN ? ` -
- -
- ` : ''} - - ${approvalQueue.length > 0 ? ` -
-
- ${approvalQueue.length} Pending Approval${approvalQueue.length > 1 ? 's' : ''} -
- ${approvalQueue.slice(0, 5).map(item => ` -
-
- ${item.moduleId} - ${item.action} -
-
- - -
-
- `).join('')} - ${approvalQueue.length > 5 ? ` -
+${approvalQueue.length - 5} more
- ` : ''} -
- ` : ''} - -
- ${approvalStats.approved} - ${approvalStats.rejected} - ${approvalStats.autoApproved} -
-
- `; - - _container.innerHTML = html; - bindEvents(); - }; - - const bindEvents = () => { - if (!_container) return; - - _container.addEventListener('click', (e) => { - const btn = e.target.closest('[data-action]'); - if (!btn) return; - - const action = btn.dataset.action; - const id = btn.dataset.id; - - switch (action) { - case 'approve': - if (id) HITLController.approve(id); - break; - case 'reject': - if (id) HITLController.reject(id, 'Rejected via widget'); - break; - } - }); - - // Handle mode change dropdown - _container.addEventListener('change', (e) => { - const el = e.target.closest('[data-action]'); - if (!el) return; - - const action = el.dataset.action; - - switch (action) { - case 'change-mode': - HITLController.setMasterMode(el.value); - break; - case 'set-steps': - const steps = parseInt(el.value, 10); - if (steps >= 1 && steps <= 100) { - HITLController.setEveryNSteps(steps); - } - break; - } - }); - }; - - // Get status for dashboard integration - const getStatus = () => { - if (!HITLController) { - return { state: 'idle', primaryMetric: 'N/A', secondaryMetric: '', message: null }; - } - - const state = HITLController.getState(); - const queue = state.approvalQueue; - const hasWarning = queue.length > 0; - const mode = state.config.masterMode; - - let primaryMetric = 'Auto'; - if (mode === 'hitl') primaryMetric = 'HITL'; - else if (mode === 'every_n') primaryMetric = `N=${state.config.everyNSteps}`; - - return { - state: hasWarning ? 'warning' : 'idle', - primaryMetric, - secondaryMetric: queue.length > 0 ? `${queue.length} pending` : 'No pending', - lastActivity: queue.length > 0 ? queue[0].timestamp : null, - message: hasWarning ? `${queue.length} approval${queue.length > 1 ? 's' : ''} needed` : null - }; - }; - - return { - init, - cleanup, - render, - getStatus - }; - } -}; - -export default HITLWidget; diff --git a/self/ui/components/toast-notifications.js b/self/ui/components/toast-notifications.js deleted file mode 100644 index 7839f6ed6..000000000 --- a/self/ui/components/toast-notifications.js +++ /dev/null @@ -1,122 +0,0 @@ -// Toast Notification System - Non-blocking user feedback -// Replaces alert() calls with elegant toast notifications -// Uses rd.css classes: toast-container, toast, toast-success/error/warning/info - -const ToastNotifications = { - metadata: { - id: 'ToastNotifications', - version: '1.0.0', - description: 'Non-blocking toast notification system for user feedback', - dependencies: ['Utils'], - async: false, - type: 'ui' - }, - - factory: (deps) => { - const { Utils } = deps; - const { logger } = Utils; - - let container = null; - let toastQueue = []; - let activeToasts = []; - - // Toast types - icons only, styling comes from rd.css - const TOAST_ICONS = { - success: '\u2605', // ★ - error: '\u2612', // ☒ - warning: '△', - info: '\u261B' // ☛ - }; - - // Initialize toast container - const init = () => { - if (container) return; - - container = document.createElement('div'); - container.id = 'toast-container'; - container.className = 'toast-container'; - document.body.appendChild(container); - logger.info('[ToastNotifications] Initialized'); - }; - - // Show toast notification - const show = (message, type = 'info', duration = 4000) => { - init(); // Ensure container exists - - const icon = TOAST_ICONS[type] || TOAST_ICONS.info; - - // Create toast element using rd.css classes - const toast = document.createElement('div'); - toast.className = `toast toast-${type}`; - - toast.innerHTML = ` - ${icon} - ${message} - - `; - - // Add to container - container.appendChild(toast); - activeToasts.push(toast); - - // Animate in using rd.css .visible class - setTimeout(() => { - toast.classList.add('visible'); - }, 10); - - // Auto-remove after duration - const removeToast = () => { - toast.classList.remove('visible'); - setTimeout(() => { - if (container && container.contains(toast)) { - container.removeChild(toast); - } - activeToasts = activeToasts.filter(t => t !== toast); - }, 300); - }; - - // Click to dismiss - toast.addEventListener('click', removeToast); - - // Auto-dismiss - if (duration > 0) { - setTimeout(removeToast, duration); - } - - return toast; - }; - - // Convenience methods - const success = (message, duration) => show(message, 'success', duration); - const error = (message, duration) => show(message, 'error', duration); - const warning = (message, duration) => show(message, 'warning', duration); - const info = (message, duration) => show(message, 'info', duration); - - // Clear all toasts - const clearAll = () => { - activeToasts.forEach(toast => { - if (container && container.contains(toast)) { - container.removeChild(toast); - } - }); - activeToasts = []; - }; - - return { - init, - show, - success, - error, - warning, - info, - clearAll - }; - } -}; - -// Register module if running in REPLOID environment -if (typeof window !== 'undefined' && window.ModuleRegistry) { - window.ModuleRegistry.register(ToastNotifications); -} - -export default ToastNotifications; diff --git a/self/ui/dashboard/metrics-dashboard.js b/self/ui/dashboard/metrics-dashboard.js deleted file mode 100644 index 5097c0cbf..000000000 --- a/self/ui/dashboard/metrics-dashboard.js +++ /dev/null @@ -1,456 +0,0 @@ -/** - * @fileoverview Metrics Dashboard - Visual performance metrics with Chart.js - * Extends PerformanceMonitor with interactive charts and visualizations - * - * @module MetricsDashboard - * @version 1.0.0 - * @category ui - * @requires Chart.js (loaded via CDN in HTML) - */ - -const MetricsDashboard = { - metadata: { - id: 'MetricsDashboard', - version: '1.0.0', - dependencies: ['Utils', 'PerformanceMonitor', 'Observability?'], - async: false, - type: 'ui' - }, - - factory: (deps) => { - const { Utils, PerformanceMonitor, Observability } = deps; - const { logger } = Utils; - - // Chart instances - let memoryChart = null; - let toolsChart = null; - let tokensChart = null; - let refreshIntervalId = null; - let _chartColors = null; - - // Read CSS variables for Chart.js theming (rd.css compliance) - const getChartColors = () => { - const styles = getComputedStyle(document.documentElement); - const fg = styles.getPropertyValue('--fg').trim() || '#000000'; - const bg = styles.getPropertyValue('--bg').trim() || '#FFFFFF'; - const opacityMuted = parseFloat(styles.getPropertyValue('--opacity-muted')) || 0.5; - const opacitySecondary = parseFloat(styles.getPropertyValue('--opacity-secondary')) || 0.6; - - // Convert hex to rgba for opacity variations - const hexToRgba = (hex, alpha) => { - const r = parseInt(hex.slice(1, 3), 16); - const g = parseInt(hex.slice(3, 5), 16); - const b = parseInt(hex.slice(5, 7), 16); - return `rgba(${r}, ${g}, ${b}, ${alpha})`; - }; - - return { - fg, - bg, - primary: hexToRgba(fg, 0.8), - primaryFill: hexToRgba(fg, 0.1), - secondary: hexToRgba(fg, opacitySecondary), - secondaryFill: hexToRgba(fg, 0.05), - grid: hexToRgba(fg, 0.1), - text: hexToRgba(fg, opacityMuted) - }; - }; - - /** - * Initialize metrics dashboard with Chart.js - * @param {HTMLElement} container - Container element for charts - */ - const init = (container) => { - if (!container) { - logger.warn('[MetricsDashboard] No container provided'); - return; - } - - // Check if Chart.js is loaded - if (typeof Chart === 'undefined') { - logger.error('[MetricsDashboard] Chart.js not loaded'); - return; - } - - logger.info('[MetricsDashboard] Initializing metrics dashboard'); - - const summaryHTML = Observability?.getDashboard ? ` -
-
-
-
Tokens
-
0
-
$0.00
-
-
-
Mutations
-
0
-
Recent changes
-
-
-
Decisions
-
0
-
Agent choices
-
-
-
Errors
-
0
-
Warnings and failures
-
-
-
-
-
Recent Mutations
-
No mutations yet
-
-
-
Recent Decisions
-
No decisions yet
-
-
-
- ` : ''; - - // Create chart canvases - const chartsHTML = ` -
-
-

Memory Usage Over Time

- -
-
-

Tool Usage

- -
-
-

LLM Token Usage

- -
-
- `; - - container.insertAdjacentHTML('beforeend', summaryHTML + chartsHTML); - - // Initialize colors from CSS variables - _chartColors = getChartColors(); - - // Initialize charts - initMemoryChart(); - initToolsChart(); - initTokensChart(); - - // Auto-refresh every 5 seconds - refreshIntervalId = setInterval(() => { - updateCharts(); - }, 5000); - updateObservabilitySummary(); - }; - - const buildChart = (canvasId, configFactory) => { - const canvas = document.getElementById(canvasId); - if (!canvas) return null; - return new Chart(canvas.getContext('2d'), configFactory()); - }; - - const baseOptions = (overrides = {}) => { - const colors = _chartColors || getChartColors(); - return { - responsive: true, - maintainAspectRatio: false, - plugins: { legend: { labels: { color: colors.text } } }, - scales: { - y: { - beginAtZero: true, - ticks: { color: colors.text }, - grid: { color: colors.grid } - }, - x: { - ticks: { color: colors.text }, - grid: { color: colors.grid } - } - }, - ...overrides - }; - }; - - const initMemoryChart = () => { - const memStats = PerformanceMonitor.getMemoryStats(); - - if (!memStats || !memStats.history) { - logger.warn('[MetricsDashboard] No memory history available'); - return; - } - - // Prepare data from history - const labels = memStats.history.map((_, i) => `${i * 30}s`); - const data = memStats.history.map(s => (s.usedJSHeapSize / 1024 / 1024).toFixed(2)); - - const colors = _chartColors || getChartColors(); - memoryChart = buildChart('memory-chart', () => ({ - type: 'line', - data: { - labels, - datasets: [{ - label: 'Memory Usage (MB)', - data, - borderColor: colors.primary, - backgroundColor: colors.primaryFill, - tension: 0.4, - fill: true - }] - }, - options: baseOptions() - })); - }; - - /** - * Initialize tool usage bar chart - */ - const initToolsChart = () => { - const metrics = PerformanceMonitor.getMetrics(); - - // Get top 10 tools by call count - const toolData = Object.entries(metrics.tools) - .map(([name, data]) => ({ - name: name.length > 20 ? name.substring(0, 20) + '...' : name, - calls: data.calls - })) - .sort((a, b) => b.calls - a.calls) - .slice(0, 10); - - const colors = _chartColors || getChartColors(); - toolsChart = buildChart('tools-chart', () => ({ - type: 'bar', - data: { - labels: toolData.map(t => t.name), - datasets: [{ - label: 'Call Count', - data: toolData.map(t => t.calls), - backgroundColor: colors.secondary, - borderColor: colors.primary, - borderWidth: 1 - }] - }, - options: baseOptions({ - scales: { - y: baseOptions().scales.y, - x: { - ticks: { color: colors.text, maxRotation: 45, minRotation: 45 }, - grid: { color: colors.grid } - } - } - }) - })); - }; - - /** - * Initialize LLM token usage doughnut chart - */ - const initTokensChart = () => { - const llmStats = PerformanceMonitor.getLLMStats(); - const colors = _chartColors || getChartColors(); - - tokensChart = buildChart('tokens-chart', () => ({ - type: 'doughnut', - data: { - labels: ['Input Tokens', 'Output Tokens'], - datasets: [{ - data: [llmStats.tokens.input, llmStats.tokens.output], - backgroundColor: [ - colors.primary, - colors.secondary - ], - borderColor: [ - colors.fg, - colors.fg - ], - borderWidth: 1 - }] - }, - options: { - responsive: true, - maintainAspectRatio: false, - plugins: { - legend: { - position: 'bottom', - labels: { color: colors.text } - } - } - } - })); - }; - - /** - * Update all charts with latest data - */ - const updateCharts = () => { - const metrics = PerformanceMonitor.getMetrics(); - const memStats = PerformanceMonitor.getMemoryStats(); - const llmStats = PerformanceMonitor.getLLMStats(); - - // Update memory chart - if (memoryChart && memStats && memStats.history) { - const labels = memStats.history.map((_, i) => `${i * 30}s`); - const data = memStats.history.map(s => (s.usedJSHeapSize / 1024 / 1024).toFixed(2)); - - memoryChart.data.labels = labels; - memoryChart.data.datasets[0].data = data; - memoryChart.update('none'); // No animation for performance - } - - // Update tools chart - if (toolsChart) { - const toolData = Object.entries(metrics.tools) - .map(([name, data]) => ({ - name: name.length > 20 ? name.substring(0, 20) + '...' : name, - calls: data.calls - })) - .sort((a, b) => b.calls - a.calls) - .slice(0, 10); - - toolsChart.data.labels = toolData.map(t => t.name); - toolsChart.data.datasets[0].data = toolData.map(t => t.calls); - toolsChart.update('none'); - } - - // Update tokens chart - if (tokensChart) { - tokensChart.data.datasets[0].data = [llmStats.tokens.input, llmStats.tokens.output]; - tokensChart.update('none'); - } - - updateObservabilitySummary(); - logger.debug('[MetricsDashboard] Charts updated'); - }; - - const updateObservabilitySummary = () => { - if (!Observability?.getDashboard) return; - const dashboard = Observability.getDashboard(); - if (!dashboard) return; - - const tokenTotal = dashboard.tokens?.session?.total || 0; - const tokenCost = dashboard.tokens?.session?.estimatedCost || 0; - const mutationTotal = dashboard.mutations?.total || 0; - const decisionTotal = dashboard.decisions?.total || 0; - const errorTotal = Array.isArray(dashboard.errors) ? dashboard.errors.length : 0; - - const tokenEl = document.getElementById('obs-token-total'); - const tokenCostEl = document.getElementById('obs-token-cost'); - const mutationEl = document.getElementById('obs-mutation-total'); - const decisionEl = document.getElementById('obs-decision-total'); - const errorEl = document.getElementById('obs-error-total'); - const mutationListEl = document.getElementById('obs-mutation-list'); - const decisionListEl = document.getElementById('obs-decision-list'); - - if (tokenEl) tokenEl.textContent = tokenTotal.toLocaleString(); - if (tokenCostEl) tokenCostEl.textContent = `$${tokenCost.toFixed(4)}`; - if (mutationEl) mutationEl.textContent = mutationTotal.toLocaleString(); - if (decisionEl) decisionEl.textContent = decisionTotal.toLocaleString(); - if (errorEl) errorEl.textContent = errorTotal.toLocaleString(); - - if (mutationListEl) { - const recentMutations = dashboard.mutations?.recent || []; - mutationListEl.innerHTML = recentMutations.length === 0 - ? 'No mutations yet' - : recentMutations.map((m) => ( - `
${m.op || 'change'} ${m.path || ''}
` - )).join(''); - } - - if (decisionListEl) { - const recentDecisions = dashboard.decisions?.recent || []; - decisionListEl.innerHTML = recentDecisions.length === 0 - ? 'No decisions yet' - : recentDecisions.map((d) => ( - `
${d.action?.toolCallCount || 0} tool calls
` - )).join(''); - } - }; - - /** - * Destroy all charts and clean up - */ - const destroy = () => { - if (memoryChart) { - memoryChart.destroy(); - memoryChart = null; - } - if (toolsChart) { - toolsChart.destroy(); - toolsChart = null; - } - if (tokensChart) { - tokensChart.destroy(); - tokensChart = null; - } - if (refreshIntervalId) { - clearInterval(refreshIntervalId); - refreshIntervalId = null; - } - logger.info('[MetricsDashboard] Destroyed'); - }; - - /** - * Generate metrics dashboard summary - * @returns {string} Markdown summary - */ - const generateSummary = () => { - const metrics = PerformanceMonitor.getMetrics(); - const llmStats = PerformanceMonitor.getLLMStats(); - const memStats = PerformanceMonitor.getMemoryStats(); - - const uptime = metrics.session.uptime; - const uptimeMin = Math.floor(uptime / 60000); - const uptimeSec = Math.floor((uptime % 60000) / 1000); - - const currentMem = memStats.current - ? (memStats.current.usedJSHeapSize / 1024 / 1024).toFixed(2) - : '0.00'; - const peakMem = memStats.max - ? (memStats.max / 1024 / 1024).toFixed(2) - : '0.00'; - const limitMem = memStats.current - ? (memStats.current.jsHeapSizeLimit / 1024 / 1024).toFixed(0) - : '0'; - - return ` -# Metrics Dashboard Summary - -**Session Uptime:** ${uptimeMin}m ${uptimeSec}s - -## LLM Usage -- **Total Calls:** ${llmStats.calls} -- **Total Tokens:** ${llmStats.tokens.total.toLocaleString()} -- **Avg Latency:** ${llmStats.avgLatency.toFixed(0)}ms -- **Error Rate:** ${(llmStats.errorRate * 100).toFixed(1)}% - -## Memory -- **Current:** ${currentMem} MB -- **Peak:** ${peakMem} MB -- **Limit:** ${limitMem} MB - -## Top Tools -${Object.entries(metrics.tools) - .sort((a, b) => b[1].calls - a[1].calls) - .slice(0, 5) - .map(([name, data]) => `- **${name}:** ${data.calls} calls (${(data.totalTime / data.calls).toFixed(1)}ms avg)`) - .join('\n')} - `.trim(); - }; - - return { - api: { - init, - updateCharts, - destroy, - generateSummary - } - }; - } -}; - -// Export for module loader -if (typeof module !== 'undefined' && module.exports) { - module.exports = MetricsDashboard; -} -MetricsDashboard; diff --git a/self/ui/dashboard/ui-manager.js b/self/ui/dashboard/ui-manager.js deleted file mode 100644 index c54279bb9..000000000 --- a/self/ui/dashboard/ui-manager.js +++ /dev/null @@ -1,127 +0,0 @@ -// UIManager - Thin orchestrator for UI panels -// Version 4.1.0: Removed legacy visualizer dependencies - -const UIManager = { - metadata: { - id: 'UIManager', - version: '1.0.0', - description: 'Orchestrates UI panels via DI Container', - dependencies: [ - 'Utils', 'EventBus', 'StateManager', - 'LLMConfigPanel', 'VFSPanel', - 'MetricsPanel', 'ChatPanel', 'CodePanel' - ], - async: true, - type: 'ui' - }, - - factory: (deps) => { - const { Utils, EventBus } = deps; - const { logger } = Utils; - - const panels = {}; - let logToggleBtn = null; - let activePanelId = 'thought-panel'; - const PANEL_STORAGE_KEY = 'reploid_active_panel'; - - const init = async () => { - logger.info('[UIManager] Initializing UI Orchestrator...'); - - logToggleBtn = document.getElementById('log-toggle-btn'); - - // Simplified panel map (removed broken visualizers) - const panelMap = [ - { id: 'local-llm-panel', module: deps.LLMConfigPanel }, - { id: 'vfs-tree', module: deps.VFSPanel }, - { id: 'performance-panel', module: deps.MetricsPanel }, - { id: 'agent-container', module: deps.ChatPanel }, - { id: 'code-panel', module: deps.CodePanel } - ]; - - for (const item of panelMap) { - if (!item.module?.init) continue; - try { - await item.module.init(item.id); - panels[item.id] = item.module; - } catch (error) { - logger.error(`[UIManager] Failed to initialize panel ${item.id}:`, error); - } - } - - setupNavigation(); - if (!restoreState()) { - showPanel(activePanelId); - } - - logger.info('[UIManager] UI Ready'); - }; - - const setupNavigation = () => { - if (logToggleBtn) { - logToggleBtn.addEventListener('click', cyclePanels); - } - - EventBus.on('panel:switch', ({ panel }) => { - if (panel) showPanel(panel); - }); - }; - - const cyclePanels = () => { - // Removed visualizers from cycle - const sequence = [ - 'thought-panel', - 'performance-panel', - 'introspection-panel', - 'local-llm-panel' - ]; - - const currentIndex = sequence.indexOf(activePanelId); - const nextIndex = (currentIndex + 1) % sequence.length; - showPanel(sequence[nextIndex]); - }; - - const showPanel = (panelId) => { - const advancedPanels = document.querySelectorAll('.advanced-panel'); - advancedPanels.forEach(panel => panel.classList.add('hidden')); - - const target = document.getElementById(panelId); - if (target) { - target.classList.remove('hidden'); - activePanelId = panelId; - if (logToggleBtn) { - logToggleBtn.textContent = `Show: ${formatPanelName(panelId)}`; - } - saveState(); - } - }; - - const formatPanelName = (id) => { - return id.replace('-panel', '').replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()); - }; - - const saveState = () => { - try { - localStorage.setItem(PANEL_STORAGE_KEY, activePanelId); - } catch (error) { - logger.warn('[UIManager] Failed to save panel state:', error); - } - }; - - const restoreState = () => { - try { - const saved = localStorage.getItem(PANEL_STORAGE_KEY); - if (saved) { - showPanel(saved); - return true; - } - } catch (error) { - logger.warn('[UIManager] Failed to restore panel state:', error); - } - return false; - }; - - return { init }; - } -}; - -export default UIManager; diff --git a/self/ui/dashboard/vfs-explorer.js b/self/ui/dashboard/vfs-explorer.js deleted file mode 100644 index e0e5b28d9..000000000 --- a/self/ui/dashboard/vfs-explorer.js +++ /dev/null @@ -1,1114 +0,0 @@ -// VFS Explorer Module for REPLOID -// Enhanced file tree with search, expand/collapse, and file viewer - -const VFSExplorer = { - metadata: { - id: 'VFSExplorer', - version: '2.0.0', - dependencies: ['Utils', 'EventBus', 'VFS', 'ToastNotifications'], - async: false, - type: 'ui' - }, - - factory: (deps) => { - const { Utils, EventBus, VFS, ToastNotifications } = deps; - const { logger, escapeHtml } = Utils; - - const BASELINE_KEY = 'REPLOID_VFS_BASELINE'; - - class Explorer { - constructor() { - this.expanded = new Set(['/']); // Track expanded folders - this.selectedFile = null; - this.selectedFiles = new Set(); // Multi-select support - this.searchTerm = ''; - this.container = null; - this.fileViewerModal = null; - this.contextMenu = null; - this.baseline = null; // Genesis baseline for state tracking - this.editMode = false; // Track if currently editing - this.sortBy = 'name'; // 'name', 'size', 'date', 'type' - this.sortAsc = true; - } - - async init(containerId) { - this.container = document.getElementById(containerId); - if (!this.container) { - logger.error(`[VFSExplorer] Container not found: ${containerId}`); - return; - } - - // Load or create baseline - await this.loadBaseline(); - - // Create context menu element - this.createContextMenu(); - - await this.render(); - - // Listen for VFS changes - EventBus.on('vfs:updated', () => this.render()); - EventBus.on('vfs:file_changed', () => this.render()); - EventBus.on('artifact:created', () => this.render()); - EventBus.on('artifact:updated', () => this.render()); - EventBus.on('artifact:deleted', () => this.render()); - - // Close context menu on click outside - document.addEventListener('click', () => this.hideContextMenu()); - } - - /** - * Create the context menu element - */ - createContextMenu() { - if (this.contextMenu) return; - this.contextMenu = document.createElement('div'); - this.contextMenu.className = 'vfs-context-menu'; - this.contextMenu.style.cssText = ` - position: fixed; - display: none; - background: var(--bg); - border: var(--border-md) solid var(--fg); - min-width: 150px; - z-index: 10000; - padding: 4px 0; - `; - document.body.appendChild(this.contextMenu); - } - - /** - * Show context menu at position - */ - showContextMenu(x, y, path, type) { - if (!this.contextMenu) return; - - const isFile = type === 'file'; - const isDeleted = this.container.querySelector(`[data-path="${path}"]`)?.dataset.state === 'deleted'; - - let menuItems = []; - - if (isFile && !isDeleted) { - menuItems = [ - { label: '✎ Edit', action: () => this.editFile(path) }, - { label: '⎘ Copy', action: () => this.copyFile(path) }, - { label: '✎ Rename', action: () => this.renameFile(path) }, - { label: '☍ Move', action: () => this.moveFile(path) }, - { label: '✄ Delete', action: () => this.deleteFile(path), danger: true } - ]; - } else if (isFile && isDeleted) { - menuItems = [ - { label: '♺ Restore', action: () => this.restoreFile(path) } - ]; - } else { - // Folder - menuItems = [ - { label: '☐ New File', action: () => this.createNewFile(path) }, - { label: '☗ New Folder', action: () => this.createNewFolder(path) }, - { label: '✎ Rename', action: () => this.renameFolder(path) }, - { label: '✄ Delete', action: () => this.deleteFolder(path), danger: true } - ]; - } - - this.contextMenu.innerHTML = menuItems.map(item => ` -
- ${item.label} -
- `).join(''); - - // Attach click handlers - this.contextMenu.querySelectorAll('.vfs-context-item').forEach((el, i) => { - el.addEventListener('click', (e) => { - e.stopPropagation(); - menuItems[i].action(); - this.hideContextMenu(); - }); - el.addEventListener('mouseenter', () => { - el.classList.add('inverted'); - }); - el.addEventListener('mouseleave', () => { - el.classList.remove('inverted'); - }); - }); - - // Position menu - this.contextMenu.style.left = `${x}px`; - this.contextMenu.style.top = `${y}px`; - this.contextMenu.style.display = 'block'; - - // Adjust if overflowing viewport - const rect = this.contextMenu.getBoundingClientRect(); - if (rect.right > window.innerWidth) { - this.contextMenu.style.left = `${window.innerWidth - rect.width - 10}px`; - } - if (rect.bottom > window.innerHeight) { - this.contextMenu.style.top = `${window.innerHeight - rect.height - 10}px`; - } - } - - hideContextMenu() { - if (this.contextMenu) { - this.contextMenu.style.display = 'none'; - } - } - - /** - * File operations - */ - async editFile(path) { - await this.showFileViewer(path, true); // Open in edit mode - } - - async copyFile(path) { - try { - const content = await VFS.read(path); - await navigator.clipboard.writeText(content); - logger.info(`[VFSExplorer] Copied ${path} to clipboard`); - if (ToastNotifications) ToastNotifications.success('Copied to clipboard'); - } catch (err) { - logger.error(`[VFSExplorer] Copy failed:`, err); - if (ToastNotifications) ToastNotifications.error('Failed to copy'); - } - } - - async renameFile(path) { - const fileName = path.split('/').pop(); - const newName = prompt('Enter new name:', fileName); - if (!newName || newName === fileName) return; - - const parentPath = path.substring(0, path.lastIndexOf('/')) || '/'; - const newPath = `${parentPath}/${newName}`; - - try { - const content = await VFS.read(path); - await VFS.write(newPath, content); - await VFS.delete(path); - logger.info(`[VFSExplorer] Renamed ${path} to ${newPath}`); - if (ToastNotifications) ToastNotifications.success(`Renamed to ${newName}`); - EventBus.emit('vfs:file_changed', { oldPath: path, newPath }); - } catch (err) { - logger.error(`[VFSExplorer] Rename failed:`, err); - if (ToastNotifications) ToastNotifications.error('Failed to rename file'); - } - } - - async moveFile(path) { - const newPath = prompt('Enter destination path:', path); - if (!newPath || newPath === path) return; - - try { - const content = await VFS.read(path); - await VFS.write(newPath, content); - await VFS.delete(path); - logger.info(`[VFSExplorer] Moved ${path} to ${newPath}`); - if (ToastNotifications) ToastNotifications.success(`Moved to ${newPath}`); - EventBus.emit('vfs:file_changed', { oldPath: path, newPath }); - } catch (err) { - logger.error(`[VFSExplorer] Move failed:`, err); - if (ToastNotifications) ToastNotifications.error('Failed to move file'); - } - } - - async deleteFile(path) { - if (!confirm(`Delete ${path}?`)) return; - - try { - await VFS.delete(path); - logger.info(`[VFSExplorer] Deleted ${path}`); - if (ToastNotifications) ToastNotifications.success('File deleted'); - EventBus.emit('vfs:file_changed', { path, deleted: true }); - } catch (err) { - logger.error(`[VFSExplorer] Delete failed:`, err); - if (ToastNotifications) ToastNotifications.error('Failed to delete file'); - } - } - - async restoreFile(path) { - // Restore from baseline if available - if (!this.baseline?.files[path]) { - if (ToastNotifications) ToastNotifications.error('No baseline data to restore from'); - return; - } - if (ToastNotifications) ToastNotifications.info('Restore requires GenesisSnapshot integration'); - logger.info(`[VFSExplorer] Restore for ${path} - requires GenesisSnapshot`); - } - - async createNewFile(folderPath) { - const fileName = prompt('Enter file name:'); - if (!fileName) return; - - const newPath = folderPath ? `${folderPath}/${fileName}` : `/${fileName}`; - - try { - await VFS.write(newPath, ''); - logger.info(`[VFSExplorer] Created file ${newPath}`); - if (ToastNotifications) ToastNotifications.success(`Created ${fileName}`); - this.expanded.add(folderPath); - await this.showFileViewer(newPath, true); // Open in edit mode - } catch (err) { - logger.error(`[VFSExplorer] Create file failed:`, err); - if (ToastNotifications) ToastNotifications.error('Failed to create file'); - } - } - - async createNewFolder(parentPath) { - const folderName = prompt('Enter folder name:'); - if (!folderName) return; - - const newPath = parentPath ? `${parentPath}/${folderName}` : `/${folderName}`; - const placeholderPath = `${newPath}/.gitkeep`; - - try { - await VFS.write(placeholderPath, ''); - logger.info(`[VFSExplorer] Created folder ${newPath}`); - if (ToastNotifications) ToastNotifications.success(`Created folder ${folderName}`); - this.expanded.add(newPath); - this.render(); - } catch (err) { - logger.error(`[VFSExplorer] Create folder failed:`, err); - if (ToastNotifications) ToastNotifications.error('Failed to create folder'); - } - } - - async renameFolder(path) { - const folderName = path.split('/').filter(p => p).pop(); - const newName = prompt('Enter new folder name:', folderName); - if (!newName || newName === folderName) return; - - // This requires moving all files in the folder - if (ToastNotifications) ToastNotifications.info('Folder rename requires moving all contents'); - logger.info(`[VFSExplorer] Folder rename for ${path} - requires batch operations`); - } - - async deleteFolder(path) { - if (!confirm(`Delete folder ${path} and all contents?`)) return; - - try { - const allPaths = await VFS.list(path); - for (const filePath of allPaths) { - await VFS.delete(filePath); - } - logger.info(`[VFSExplorer] Deleted folder ${path} (${allPaths.length} files)`); - if (ToastNotifications) ToastNotifications.success(`Deleted folder with ${allPaths.length} files`); - this.expanded.delete(path); - this.render(); - } catch (err) { - logger.error(`[VFSExplorer] Delete folder failed:`, err); - if (ToastNotifications) ToastNotifications.error('Failed to delete folder'); - } - } - - /** - * Load baseline from localStorage or create one - */ - async loadBaseline() { - try { - const stored = localStorage.getItem(BASELINE_KEY); - if (stored) { - this.baseline = JSON.parse(stored); - logger.debug(`[VFSExplorer] Loaded baseline with ${Object.keys(this.baseline.files).length} files`); - } - } catch (e) { - logger.warn('[VFSExplorer] Could not load baseline:', e.message); - } - } - - /** - * Create a new baseline snapshot (call at genesis) - */ - async createBaseline() { - try { - const allMeta = await this.getAllFileMetadata(); - this.baseline = { - timestamp: Date.now(), - files: {} - }; - for (const path in allMeta) { - this.baseline.files[path] = { - size: allMeta[path].size, - updated: allMeta[path].updated - }; - } - localStorage.setItem(BASELINE_KEY, JSON.stringify(this.baseline)); - logger.info(`[VFSExplorer] Created baseline with ${Object.keys(this.baseline.files).length} files`); - return this.baseline; - } catch (e) { - logger.error('[VFSExplorer] Failed to create baseline:', e.message); - return null; - } - } - - /** - * Get all file metadata from VFS - */ - async getAllFileMetadata() { - const allPaths = await VFS.list('/'); - const metadata = {}; - for (const path of allPaths) { - try { - const stat = await VFS.stat(path); - if (stat && stat.type === 'file') { - metadata[path] = stat; - } - } catch (e) { - // Skip files we can't stat - } - } - return metadata; - } - - /** - * Get file state relative to baseline - */ - getFileState(path, currentMeta) { - if (!this.baseline) return null; - - const baselineFile = this.baseline.files[path]; - - if (!baselineFile) { - return 'created'; // New file since genesis - } - - if (currentMeta.updated > baselineFile.updated || currentMeta.size !== baselineFile.size) { - return 'modified'; // Modified since genesis - } - - return null; // Unchanged - } - - async render() { - if (!this.container) return; - - const allMeta = await this.getAllFileMetadata(); - - // Add state info based on baseline comparison - for (const path in allMeta) { - allMeta[path].state = this.getFileState(path, allMeta[path]); - } - - // Check for deleted files (in baseline but not in current) - if (this.baseline) { - for (const path in this.baseline.files) { - if (!allMeta[path]) { - allMeta[path] = { - size: this.baseline.files[path].size, - state: 'deleted', - type: 'file' - }; - } - } - } - - const tree = this.buildTree(allMeta); - - const selectedCount = this.selectedFiles.size; - const selectionInfo = selectedCount > 0 ? ` | ${selectedCount} selected` : ''; - - this.container.innerHTML = ` -
- -
${this.renderTree(tree)}
-
- ${Object.keys(allMeta).length} files${selectionInfo} -
-
- `; - - this.attachEventListeners(); - } - - buildTree(allMeta) { - const tree = { - name: 'root', - path: '', - type: 'folder', - children: [] - }; - - for (const path in allMeta) { - const parts = path.split('/').filter(p => p); - let current = tree; - - parts.forEach((part, index) => { - const isLast = index === parts.length - 1; - - if (isLast) { - // File node - current.children.push({ - name: part, - path: path, - type: 'file', - size: allMeta[path].size || 0, - metadata: allMeta[path] - }); - } else { - // Folder node - let folder = current.children.find(c => c.name === part && c.type === 'folder'); - if (!folder) { - folder = { - name: part, - path: parts.slice(0, index + 1).join('/'), - type: 'folder', - children: [] - }; - current.children.push(folder); - } - current = folder; - } - }); - } - - // Sort: folders first, then files by configured sort field - const sortChildren = (node) => { - if (node.children) { - node.children.sort((a, b) => { - // Folders always first - if (a.type !== b.type) { - return a.type === 'folder' ? -1 : 1; - } - - let result = 0; - switch (this.sortBy) { - case 'size': - result = (a.size || 0) - (b.size || 0); - break; - case 'date': - const aDate = a.metadata?.updated || 0; - const bDate = b.metadata?.updated || 0; - result = aDate - bDate; - break; - case 'type': - const aExt = a.name.split('.').pop() || ''; - const bExt = b.name.split('.').pop() || ''; - result = aExt.localeCompare(bExt); - break; - case 'name': - default: - result = a.name.localeCompare(b.name); - } - return this.sortAsc ? result : -result; - }); - node.children.forEach(sortChildren); - } - }; - sortChildren(tree); - - return tree; - } - - renderTree(node, depth = 0) { - if (!node.children || node.children.length === 0) { - return ''; - } - - const filteredChildren = this.searchTerm - ? node.children.filter(child => this.matchesSearch(child)) - : node.children; - - return filteredChildren.map(child => { - if (child.type === 'file') { - return this.renderFile(child, depth); - } else { - return this.renderFolder(child, depth); - } - }).join(''); - } - - renderFile(node, depth) { - const icon = this.getFileIcon(node.path); - const selected = node.path === this.selectedFile ? 'selected' : ''; - const multiSelected = this.selectedFiles.has(node.path) ? 'multi-selected inverted' : ''; - const highlight = this.searchTerm && node.name.toLowerCase().includes(this.searchTerm.toLowerCase()) - ? 'highlight' : ''; - - // File state classes based on metadata - const fileState = this.getFileStateClass(node.metadata); - const stateIcon = this.getFileStateIcon(node.metadata); - - return ` -
- ${multiSelected ? '' : ''} - - ${escapeHtml(node.name)} - ${stateIcon ? `${stateIcon}` : ''} - ${this.formatSize(node.size)} -
- `; - } - - getFileStateClass(metadata) { - if (!metadata?.state) return ''; - switch (metadata.state) { - case 'created': return 'vfs-file-created'; - case 'modified': return 'vfs-file-modified'; - case 'deleted': return 'vfs-file-deleted'; - default: return ''; - } - } - - getFileStateIcon(metadata) { - if (!metadata?.state) return ''; - switch (metadata.state) { - case 'created': return '+'; - case 'modified': return '~'; - case 'deleted': return '✄'; - default: return ''; - } - } - - renderFolder(node, depth) { - const isExpanded = this.expanded.has(node.path) || this.searchTerm !== ''; - const icon = isExpanded ? '☗' : '☗'; - const expandIcon = isExpanded ? 'v' : '>'; - - const childrenHtml = isExpanded ? this.renderTree(node, depth + 1) : ''; - const fileCount = this.countFiles(node); - - return ` -
-
- - - ${escapeHtml(node.name)} - -
-
- ${childrenHtml} -
-
- `; - } - - countFiles(node) { - if (node.type === 'file') return 1; - if (!node.children) return 0; - return node.children.reduce((sum, child) => sum + this.countFiles(child), 0); - } - - getFileIcon(path) { - const ext = path.split('.').pop().toLowerCase(); - const iconMap = { - 'js': 'ƒ', - 'json': '☷', - 'md': '☐', - 'css': '☲', - 'html': '☊', - 'txt': '☐', - 'yml': '⎈', - 'yaml': '⎈', - 'xml': '☐', - 'svg': '☻', - 'png': '☻', - 'jpg': '☻', - 'jpeg': '☻', - 'gif': '☻', - 'pdf': '☙', - 'zip': '⛝', - 'tar': '⛝', - 'gz': '⛝' - }; - return iconMap[ext] || '☐'; - } - - formatSize(bytes) { - if (!bytes || bytes === 0) return '0 B'; - const k = 1024; - const sizes = ['B', 'KB', 'MB', 'GB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i]; - } - - matchesSearch(node) { - if (!this.searchTerm) return true; - const term = this.searchTerm.toLowerCase(); - - // Search in name and path - if (node.name.toLowerCase().includes(term)) return true; - if (node.path.toLowerCase().includes(term)) return true; - - // Search in children - if (node.children) { - return node.children.some(child => this.matchesSearch(child)); - } - - return false; - } - - attachEventListeners() { - // Search input - const searchInput = this.container.querySelector('.vfs-search'); - if (searchInput) { - searchInput.addEventListener('input', (e) => { - this.searchTerm = e.target.value; - this.render(); - }); - - // Keyboard shortcuts: Ctrl+F or Cmd+F to focus search - document.addEventListener('keydown', (e) => { - if ((e.ctrlKey || e.metaKey) && e.key === 'f' && !e.shiftKey) { - const explorerVisible = this.container && this.container.offsetParent !== null; - if (explorerVisible) { - e.preventDefault(); - searchInput.focus(); - } - } - // ESC to clear search - if (e.key === 'Escape' && document.activeElement === searchInput && this.searchTerm) { - e.preventDefault(); - this.searchTerm = ''; - searchInput.value = ''; - this.render(); - } - }); - } - - // Collapse all button - const collapseBtn = this.container.querySelector('.vfs-collapse-all'); - if (collapseBtn) { - collapseBtn.addEventListener('click', () => { - this.expanded.clear(); - this.render(); - }); - } - - // Expand all button - const expandBtn = this.container.querySelector('.vfs-expand-all'); - if (expandBtn) { - expandBtn.addEventListener('click', async () => { - const allMeta = await this.getAllFileMetadata(); - const tree = this.buildTree(allMeta); - this.expandAll(tree); - this.render(); - }); - } - - // Sort select - const sortSelect = this.container.querySelector('.vfs-sort'); - if (sortSelect) { - sortSelect.addEventListener('change', (e) => { - this.sortBy = e.target.value; - this.render(); - }); - } - - // Sort direction toggle - const sortDirBtn = this.container.querySelector('.vfs-sort-dir'); - if (sortDirBtn) { - sortDirBtn.addEventListener('click', () => { - this.sortAsc = !this.sortAsc; - this.render(); - }); - } - - // New file button - const newFileBtn = this.container.querySelector('.vfs-new-file'); - if (newFileBtn) { - newFileBtn.addEventListener('click', () => { - this.createNewFile('/'); - }); - } - - // Folder click handlers - this.container.querySelectorAll('.vfs-folder-header').forEach(header => { - header.addEventListener('click', (e) => { - e.stopPropagation(); - const path = header.dataset.path; - if (this.expanded.has(path)) { - this.expanded.delete(path); - } else { - this.expanded.add(path); - } - this.render(); - }); - - // Right-click context menu for folders - header.addEventListener('contextmenu', (e) => { - e.preventDefault(); - e.stopPropagation(); - const path = header.dataset.path; - this.showContextMenu(e.clientX, e.clientY, path, 'folder'); - }); - - // Keyboard navigation: Enter/Space to toggle folder - header.addEventListener('keydown', (e) => { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - const path = header.dataset.path; - if (this.expanded.has(path)) { - this.expanded.delete(path); - } else { - this.expanded.add(path); - } - this.render(); - } - }); - }); - - // File click handlers - this.container.querySelectorAll('.vfs-file').forEach(fileItem => { - fileItem.addEventListener('click', async (e) => { - e.stopPropagation(); - const path = fileItem.dataset.path; - - // Multi-select with Ctrl/Cmd - if (e.ctrlKey || e.metaKey) { - if (this.selectedFiles.has(path)) { - this.selectedFiles.delete(path); - } else { - this.selectedFiles.add(path); - } - this.selectedFile = path; - this.render(); - return; - } - - // Clear multi-select on regular click - this.selectedFiles.clear(); - this.selectedFile = path; - await this.showFileViewer(path); - this.render(); - }); - - // Right-click context menu for files - fileItem.addEventListener('contextmenu', (e) => { - e.preventDefault(); - e.stopPropagation(); - const path = fileItem.dataset.path; - this.selectedFile = path; - this.showContextMenu(e.clientX, e.clientY, path, 'file'); - }); - - // Keyboard navigation: Enter to open file - fileItem.addEventListener('keydown', async (e) => { - if (e.key === 'Enter') { - e.preventDefault(); - const path = fileItem.dataset.path; - this.selectedFile = path; - await this.showFileViewer(path); - this.render(); - } - // Delete key to delete selected file - if (e.key === 'Delete' || e.key === 'Backspace') { - e.preventDefault(); - const path = fileItem.dataset.path; - await this.deleteFile(path); - } - }); - }); - } - - expandAll(node) { - if (node.type === 'folder') { - this.expanded.add(node.path); - if (node.children) { - node.children.forEach(child => this.expandAll(child)); - } - } - } - - async showFileViewer(path, editMode = false) { - try { - const content = await VFS.read(path); - const metadata = await VFS.stat(path); - - // Create modal if it doesn't exist - if (!this.fileViewerModal) { - this.fileViewerModal = document.createElement('div'); - this.fileViewerModal.className = 'vfs-file-viewer-modal'; - document.body.appendChild(this.fileViewerModal); - } - - const language = this.getLanguageFromPath(path); - this.editMode = editMode; - - // Build body content based on mode - const bodyContent = editMode - ? `` - : `
${escapeHtml(content || '')}
`; - - // Build footer buttons based on mode - const footerButtons = editMode - ? ` - ` - : ` - - `; - - this.fileViewerModal.innerHTML = ` -
-
-
-
- ${this.getFileIcon(path)} - ${escapeHtml(path)} - ${editMode ? '[Editing]' : ''} -
- -
-
- Type: ${metadata?.type || 'unknown'} | - Size: ${this.formatSize(content?.length || 0)} | - Lines: ${(content || '').split('\n').length} -
-
- ${bodyContent} -
- -
- `; - - this.fileViewerModal.style.display = 'flex'; - - // Focus editor if in edit mode - if (editMode) { - const editor = this.fileViewerModal.querySelector('.vfs-editor'); - if (editor) { - setTimeout(() => editor.focus(), 100); - - // Handle Tab key for indentation - editor.addEventListener('keydown', (e) => { - if (e.key === 'Tab') { - e.preventDefault(); - const start = editor.selectionStart; - const end = editor.selectionEnd; - editor.value = editor.value.substring(0, start) + ' ' + editor.value.substring(end); - editor.selectionStart = editor.selectionEnd = start + 2; - } - // Ctrl/Cmd+S to save - if ((e.ctrlKey || e.metaKey) && e.key === 's') { - e.preventDefault(); - this.fileViewerModal.querySelector('.vfs-file-viewer-save')?.click(); - } - }); - } - } - - // Close button - this.fileViewerModal.querySelector('.vfs-file-viewer-close').addEventListener('click', () => { - this.closeFileViewer(); - }); - - // Overlay click to close - this.fileViewerModal.querySelector('.vfs-file-viewer-overlay').addEventListener('click', () => { - this.closeFileViewer(); - }); - - // Edit mode buttons - if (editMode) { - this.fileViewerModal.querySelector('.vfs-file-viewer-save')?.addEventListener('click', async () => { - const editor = this.fileViewerModal.querySelector('.vfs-editor'); - if (editor) { - try { - await VFS.write(path, editor.value); - logger.info(`[VFSExplorer] Saved ${path}`); - if (ToastNotifications) ToastNotifications.success('File saved'); - EventBus.emit('vfs:file_changed', { path }); - this.editMode = false; - await this.showFileViewer(path, false); // Switch back to view mode - } catch (err) { - logger.error(`[VFSExplorer] Save failed:`, err); - if (ToastNotifications) ToastNotifications.error('Failed to save file'); - } - } - }); - - this.fileViewerModal.querySelector('.vfs-file-viewer-cancel')?.addEventListener('click', () => { - this.editMode = false; - this.showFileViewer(path, false); // Switch back to view mode - }); - } else { - // View mode buttons - this.fileViewerModal.querySelector('.vfs-file-viewer-copy')?.addEventListener('click', async (e) => { - try { - await navigator.clipboard.writeText(content); - logger.info(`[VFSExplorer] Copied ${path} to clipboard`); - - // Visual feedback - const btn = e.target; - const originalText = btn.innerHTML; - btn.innerHTML = '✓ Copied!'; - btn.classList.add('inverted'); - setTimeout(() => { - btn.innerHTML = originalText; - btn.classList.remove('inverted'); - }, 2000); - } catch (err) { - logger.error(`[VFSExplorer] Failed to copy to clipboard:`, err); - if (ToastNotifications) ToastNotifications.error('Failed to copy to clipboard'); - } - }); - - this.fileViewerModal.querySelector('.vfs-file-viewer-history')?.addEventListener('click', async () => { - logger.info(`[VFSExplorer] History for ${path} - feature requires GenesisSnapshot integration`); - if (ToastNotifications) ToastNotifications.info('File history available via GenesisSnapshot module'); - }); - - this.fileViewerModal.querySelector('.vfs-file-viewer-edit')?.addEventListener('click', () => { - this.showFileViewer(path, true); // Switch to edit mode - }); - } - - // ESC key to close (with unsaved changes warning in edit mode) - const handleEsc = (e) => { - if (e.key === 'Escape' && this.fileViewerModal.style.display === 'flex') { - this.closeFileViewer(); - document.removeEventListener('keydown', handleEsc); - } - }; - document.addEventListener('keydown', handleEsc); - - } catch (error) { - logger.error(`[VFSExplorer] Failed to load file ${path}:`, error); - if (ToastNotifications) ToastNotifications.error(`Failed to load file: ${error.message}`); - } - } - - closeFileViewer() { - if (this.editMode) { - const editor = this.fileViewerModal?.querySelector('.vfs-editor'); - if (editor && editor.value !== editor.defaultValue) { - if (!confirm('Discard unsaved changes?')) return; - } - } - this.editMode = false; - if (this.fileViewerModal) { - this.fileViewerModal.style.display = 'none'; - } - } - - getLanguageFromPath(path) { - const ext = path.split('.').pop().toLowerCase(); - const langMap = { - 'js': 'javascript', - 'json': 'json', - 'md': 'markdown', - 'css': 'css', - 'html': 'html', - 'txt': 'text', - 'yml': 'yaml', - 'yaml': 'yaml', - 'xml': 'xml', - 'py': 'python', - 'rb': 'ruby', - 'java': 'java', - 'go': 'go', - 'rs': 'rust', - 'c': 'c', - 'cpp': 'cpp', - 'sh': 'bash' - }; - return langMap[ext] || 'text'; - } - - } - - const explorer = new Explorer(); - - return { - api: { - init: (containerId) => explorer.init(containerId), - render: () => explorer.render(), - setSearchTerm: (term) => { - explorer.searchTerm = term; - explorer.render(); - }, - expandPath: (path) => { - explorer.expanded.add(path); - explorer.render(); - }, - collapsePath: (path) => { - explorer.expanded.delete(path); - explorer.render(); - }, - selectFile: (path) => { - explorer.selectedFile = path; - explorer.showFileViewer(path); - }, - // File operations - editFile: (path) => explorer.editFile(path), - createFile: (folderPath) => explorer.createNewFile(folderPath), - createFolder: (parentPath) => explorer.createNewFolder(parentPath), - deleteFile: (path) => explorer.deleteFile(path), - deleteFolder: (path) => explorer.deleteFolder(path), - renameFile: (path) => explorer.renameFile(path), - moveFile: (path) => explorer.moveFile(path), - copyFileToClipboard: (path) => explorer.copyFile(path), - // Multi-select - getSelectedFiles: () => Array.from(explorer.selectedFiles), - clearSelection: () => { - explorer.selectedFiles.clear(); - explorer.selectedFile = null; - explorer.render(); - }, - selectMultiple: (paths) => { - paths.forEach(p => explorer.selectedFiles.add(p)); - explorer.render(); - }, - // Sorting - setSortBy: (field, ascending = true) => { - explorer.sortBy = field; - explorer.sortAsc = ascending; - explorer.render(); - }, - // Baseline management for file state tracking - createBaseline: () => explorer.createBaseline(), - hasBaseline: () => !!explorer.baseline, - clearBaseline: () => { - explorer.baseline = null; - localStorage.removeItem(BASELINE_KEY); - explorer.render(); - } - } - }; - } -}; - -// Export -export default VFSExplorer; diff --git a/self/ui/panels/metrics-panel.js b/self/ui/panels/metrics-panel.js deleted file mode 100644 index 5a87d1a47..000000000 --- a/self/ui/panels/metrics-panel.js +++ /dev/null @@ -1,48 +0,0 @@ -const MetricsPanel = { - metadata: { - id: 'MetricsPanel', - version: '1.0.0', - dependencies: ['Utils', 'MetricsDashboard', 'PerformanceMonitor', 'ToastNotifications?'], - async: false, - type: 'ui' - }, - - factory: (deps) => { - const { Utils, MetricsDashboard, PerformanceMonitor, ToastNotifications } = deps; - const { logger, exportAsMarkdown } = Utils; - - const init = (containerId) => { - const container = document.getElementById(containerId); - if (!container) return; - - if (MetricsDashboard?.init) { - MetricsDashboard.init(container); - } - - const refreshBtn = document.getElementById('perf-refresh-btn'); - const exportBtn = document.getElementById('perf-export-btn'); - - if (refreshBtn) { - refreshBtn.onclick = () => { - MetricsDashboard?.updateCharts?.(); - }; - } - - if (exportBtn) { - exportBtn.onclick = () => { - const report = PerformanceMonitor?.generateReport?.(); - if (report) { - exportAsMarkdown(`performance-${Date.now()}.md`, report); - if (ToastNotifications) ToastNotifications.success('Performance report exported'); - } - }; - } - - logger.info('[MetricsPanel] Initialized'); - }; - - return { init }; - } -}; - -export default MetricsPanel; diff --git a/self/ui/proto.js b/self/ui/proto.js deleted file mode 100644 index 57593c963..000000000 --- a/self/ui/proto.js +++ /dev/null @@ -1,6 +0,0 @@ -/** - * @fileoverview Proto UI - Re-export from modular version - * This file maintains backward compatibility while using the modular implementation. - */ - -export { default } from './proto/index.js'; diff --git a/self/ui/proto/schemas.js b/self/ui/proto/schemas.js deleted file mode 100644 index 35f1430f4..000000000 --- a/self/ui/proto/schemas.js +++ /dev/null @@ -1,114 +0,0 @@ -/** - * Proto Schemas - Schema registry panel logic - */ - -export const createSchemaManager = (deps) => { - const { logger, escapeHtml } = deps; - - let _schemaRegistrySvc = null; - let _toolSchemas = []; - let _workerSchemas = []; - let _schemaSearch = ''; - let _schemaLoaded = false; - - const resolveSchemaRegistry = async () => { - if (_schemaRegistrySvc) return _schemaRegistrySvc; - try { - _schemaRegistrySvc = window.REPLOID?.schemaRegistry - || (await window.REPLOID_DI?.resolve?.('SchemaRegistry')); - } catch (e) { - logger.warn('[Proto] SchemaRegistry unavailable', e?.message || e); - } - return _schemaRegistrySvc; - }; - - const renderSchemaPanel = () => { - const toolList = document.getElementById('schema-tool-list'); - const workerList = document.getElementById('schema-worker-list'); - const toolCountEl = document.getElementById('schema-tool-count'); - const workerCountEl = document.getElementById('schema-worker-count'); - if (!toolList || !workerList) return; - - const query = _schemaSearch.trim().toLowerCase(); - const filteredTools = _toolSchemas.filter(entry => entry.name.toLowerCase().includes(query)); - const filteredWorkers = _workerSchemas.filter(entry => entry.name.toLowerCase().includes(query)); - - if (toolCountEl) toolCountEl.textContent = `${filteredTools.length} tools`; - if (workerCountEl) workerCountEl.textContent = `${filteredWorkers.length} worker types`; - - toolList.innerHTML = filteredTools.length === 0 - ? '
No tool schemas match your search
' - : filteredTools.map(entry => { - const description = entry.schema?.description || 'No description'; - const payload = entry.schema?.parameters ? JSON.stringify(entry.schema.parameters, null, 2) : '{}'; - const badge = entry.builtin ? 'core' : ''; - return ` -
-
-
- ${escapeHtml(entry.name)} - ${badge} -
- ${escapeHtml(description)} -
-
${escapeHtml(payload)}
-
- `; - }).join(''); - - workerList.innerHTML = filteredWorkers.length === 0 - ? '
No worker definitions match your search
' - : filteredWorkers.map(entry => { - const config = entry.config || {}; - const badge = entry.builtin ? 'core' : ''; - const toolSummary = config.tools === '*' - ? 'All tools' - : (config.tools || []).map(t => `${escapeHtml(t)}`).join(''); - return ` -
-
-
- ${escapeHtml(entry.name)} - ${badge} -
- ${escapeHtml(config.description || '')} -
-
-
Default role: ${escapeHtml(config.defaultModelRole || '-')}
-
Can spawn: ${config.canSpawnWorkers ? 'Yes' : 'No'}
-
-
${toolSummary || 'No tools configured'}
-
- `; - }).join(''); - }; - - const refreshSchemaData = async () => { - const svc = await resolveSchemaRegistry(); - if (!svc?.listToolSchemas) { - const toolList = document.getElementById('schema-tool-list'); - if (toolList) toolList.innerHTML = '
Schema registry unavailable
'; - return; - } - try { - _toolSchemas = svc.listToolSchemas() || []; - _workerSchemas = svc.listWorkerTypes?.() || []; - _schemaLoaded = true; - renderSchemaPanel(); - } catch (e) { - logger.warn('[Proto] Failed to load schema registry', e?.message || e); - } - }; - - const setSearch = (query) => { - _schemaSearch = query || ''; - renderSchemaPanel(); - }; - - return { - refreshSchemaData, - renderSchemaPanel, - setSearch, - isLoaded: () => _schemaLoaded - }; -}; diff --git a/server/pool/policy-router.js b/server/pool/policy-router.js index da7d64a46..22ab81e0d 100644 --- a/server/pool/policy-router.js +++ b/server/pool/policy-router.js @@ -11,76 +11,30 @@ import { } from './config.js'; import { validateLaunchModelRequirement } from './model-contract.js'; import { isSequenceWorkload } from '../../self/pool/sequence-workload.js'; +import { + POOLDAY_POLICY_CLASSES, + classifyPooldayPrompt, + validateGenerationConfig, + validatePooldayPolicyClasses +} from '../../self/pool/policy-validation.js'; -export { DETERMINISTIC_GENERATION_CONFIG, POLICIES, POLICY_IDS, getPolicy, listPolicies }; +export { + DETERMINISTIC_GENERATION_CONFIG, + POLICIES, + POLICY_IDS, + getPolicy, + listPolicies, + POOLDAY_POLICY_CLASSES, + classifyPooldayPrompt, + validatePooldayPolicyClasses +}; export const FASTEST_RECEIPT_POLICY = POLICIES[POLICY_IDS.fastestReceipt]; export const CANARY_AUDITED_POLICY = POLICIES[POLICY_IDS.canaryAudited]; export const REDUNDANT_AGREEMENT_POLICY = POLICIES[POLICY_IDS.redundantAgreement]; export const RING_QUORUM_RECEIPT_POLICY = POLICIES[POLICY_IDS.ringQuorumReceipt]; -export const POOLDAY_POLICY_CLASSES = Object.freeze({ - publicText: 'public_text', - codeHelp: 'code_help', - benchmarkEval: 'benchmark_eval', - pii: 'pii', - secrets: 'secrets', - medicalPrivate: 'medical_private', - illegalContent: 'illegal_content' -}); - -const BLOCKED_PUBLIC_PROVIDER_CLASSES = new Set([ - POOLDAY_POLICY_CLASSES.pii, - POOLDAY_POLICY_CLASSES.secrets, - POOLDAY_POLICY_CLASSES.medicalPrivate, - POOLDAY_POLICY_CLASSES.illegalContent -]); - -export function classifyPooldayPrompt(prompt = '') { - const text = String(prompt || ''); - const classes = new Set([POOLDAY_POLICY_CLASSES.publicText]); - if (/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/i.test(text)) classes.add(POOLDAY_POLICY_CLASSES.pii); - if (/\b(api[_-]?key|secret|password|private[_-]?key|token)\b\s*[:=]/i.test(text)) classes.add(POOLDAY_POLICY_CLASSES.secrets); - if (/\b(sk-[a-z0-9]{12,}|AIza[0-9A-Za-z_-]{20,})\b/.test(text)) classes.add(POOLDAY_POLICY_CLASSES.secrets); - if (/\b(patient|diagnosis|medical record|prescription)\b/i.test(text)) classes.add(POOLDAY_POLICY_CLASSES.medicalPrivate); - if (/\b(malware|credential theft|phishing kit|exploit chain)\b/i.test(text)) classes.add(POOLDAY_POLICY_CLASSES.illegalContent); - return Object.freeze({ - classes: [...classes], - blockedClasses: [...classes].filter((policyClass) => BLOCKED_PUBLIC_PROVIDER_CLASSES.has(policyClass)), - publicProviderSafe: [...classes].every((policyClass) => !BLOCKED_PUBLIC_PROVIDER_CLASSES.has(policyClass)) - }); -} - -export function validatePooldayPolicyClasses(request = {}) { - const reasons = []; - const classification = classifyPooldayPrompt(request.prompt || ''); - const explicitTags = Array.isArray(request.policyTags) ? request.policyTags.map(String) : []; - const blockedTags = explicitTags.filter((tag) => BLOCKED_PUBLIC_PROVIDER_CLASSES.has(tag)); - if (classification.blockedClasses.length > 0) { - reasons.push(`prompt policy classes are not allowed for public browser providers: ${classification.blockedClasses.join(', ')}`); - } - if (blockedTags.length > 0) { - reasons.push(`policyTags are not allowed for public browser providers: ${blockedTags.join(', ')}`); - } - return { - ok: reasons.length === 0, - reasons, - classification: { - ...classification, - explicitTags - } - }; -} - export function validateDeterministicGenerationConfig(config = {}) { - const reasons = []; - const allowedKeys = new Set(Object.keys(DETERMINISTIC_GENERATION_CONFIG)); - for (const [key, expected] of Object.entries(DETERMINISTIC_GENERATION_CONFIG)) { - if (config[key] !== expected) reasons.push(`generationConfig.${key} must be ${expected}`); - } - for (const key of Object.keys(config || {})) { - if (!allowedKeys.has(key)) reasons.push(`generationConfig.${key} is not allowed`); - } - return reasons; + return validateGenerationConfig(config, DETERMINISTIC_GENERATION_CONFIG); } export function validateJobRequest(request = {}) { @@ -96,6 +50,13 @@ export function validateJobRequest(request = {}) { if (!request.modelRequirements?.manifestHash) reasons.push('modelRequirements.manifestHash is required'); if (!request.modelRequirements?.runtime) reasons.push('modelRequirements.runtime is required'); if (!request.modelRequirements?.backend) reasons.push('modelRequirements.backend is required'); + if ( + policy + && request.modelRequirements?.modelId + && !policy.allowedModels?.includes(request.modelRequirements.modelId) + ) { + reasons.push(`model ${request.modelRequirements.modelId} is not allowed by policy ${policyId}`); + } if (isSequenceWorkload(request.modelRequirements?.workload)) { reasons.push('biological sequence jobs require the peer-room WebRTC input lane'); } diff --git a/tests/e2e/reploid-lab-helpers.js b/tests/e2e/reploid-lab-helpers.js index f8d938f77..24ba04c5b 100644 --- a/tests/e2e/reploid-lab-helpers.js +++ b/tests/e2e/reploid-lab-helpers.js @@ -1,5 +1,5 @@ import { expect } from '@playwright/test'; -import { getLabRouteCases } from '../../self/config/lab-route-profiles.js'; +import { getLabRouteCases } from '../../self/lab/profiles.js'; export const DB_PREFIX = 'reploid-vfs-v0'; diff --git a/tests/integration/doppler-arena.test.js b/tests/integration/doppler-arena.test.js deleted file mode 100644 index 457bd7825..000000000 --- a/tests/integration/doppler-arena.test.js +++ /dev/null @@ -1,530 +0,0 @@ -/** - * Doppler-Arena Integration Tests (Tier 3 P1) - * - * Tests the wiring between Doppler LoRA adapters and ArenaHarness: - * - Adapter loading and switching - * - Arena expert pool competitions with adapters - * - passRate measurement with adapter switching - * - Adapter composition (merge strategies) - * - * Run: - * npm test -- --grep "Doppler Arena" - */ - -import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest'; - -// Mock implementations for testing without real Doppler -const createMockEventBus = () => { - const listeners = new Map(); - return { - emit(event, data) { - const handlers = listeners.get(event) || []; - handlers.forEach(fn => fn(data)); - }, - on(event, fn) { - if (!listeners.has(event)) listeners.set(event, []); - listeners.get(event).push(fn); - }, - off(event, fn) { - const handlers = listeners.get(event) || []; - const idx = handlers.indexOf(fn); - if (idx >= 0) handlers.splice(idx, 1); - }, - }; -}; - -const createMockUtils = () => ({ - logger: { - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), - debug: vi.fn(), - }, - generateId: (prefix) => `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, -}); - -const createMockArenaHarness = () => ({ - scoreOutput: (output, task, options = {}) => { - // Simple scoring: check if output is valid JSON if schema provided - if (task.schema) { - try { - const parsed = typeof output === 'string' ? JSON.parse(output) : output; - // Check required fields - const required = task.schema.required || []; - const hasAllRequired = required.every(field => field in parsed); - return { - score: hasAllRequired ? 0.8 : 0.3, - valid: hasAllRequired, - errors: hasAllRequired ? [] : ['Missing required fields'], - parsed, - }; - } catch (e) { - return { score: 0, valid: false, errors: ['Invalid JSON'], parsed: null }; - } - } - // Default: score based on output length (simple heuristic) - return { - score: Math.min(1, (output?.length || 0) / 100), - valid: true, - errors: [], - }; - }, -}); - -// Create module instance with mocks -const createDopplerArenaIntegration = () => { - const EventBus = createMockEventBus(); - const Utils = createMockUtils(); - const ArenaHarness = createMockArenaHarness(); - - // Simulated adapter cache - const adapterCache = new Map(); - let activeAdapter = null; - let baseModelId = null; - - return { - EventBus, - Utils, - ArenaHarness, - - async initDoppler() { - return true; - }, - - async loadBaseModel(modelId, modelUrl = null, options = {}) { - baseModelId = modelId; - return true; - }, - - async loadAdapter(adapterId, manifest) { - adapterCache.set(adapterId, manifest); - activeAdapter = adapterId; - return adapterId; - }, - - async switchAdapter(adapterId) { - if (adapterId !== null && !adapterCache.has(adapterId)) { - throw new Error(`Adapter not loaded: ${adapterId}`); - } - activeAdapter = adapterId; - return adapterId; - }, - - async runInference(prompt, options = {}) { - // Simulate different outputs based on adapter - const adapterBonus = activeAdapter ? 10 : 0; - const baseLength = 50 + Math.random() * 50 + adapterBonus; - const content = 'A'.repeat(Math.floor(baseLength)); - - return { - content, - durationMs: 100 + Math.random() * 50, - tokensGenerated: Math.floor(baseLength / 4), - tokPerSec: 50 + Math.random() * 20 + adapterBonus, - adapter: activeAdapter, - }; - }, - - createExpert(adapterId, options = {}) { - return { - id: adapterId || 'base-model', - adapter: adapterId, - name: options.name || adapterId || 'Base Model', - modelId: baseModelId, - weight: options.weight || 1.0, - temperature: options.temperature, - maxTokens: options.maxTokens, - }; - }, - - async runAdapterCompetition(task, experts, options = {}) { - const runId = Utils.generateId('arena-adapter'); - - EventBus.emit('arena:adapter:start', { - runId, - expertCount: experts.length, - task: task.prompt?.slice(0, 100), - }); - - const results = []; - - for (const expert of experts) { - const expertResult = { - expert, - output: null, - score: { score: 0, valid: true, errors: [] }, - durationMs: 0, - tokPerSec: 0, - }; - - try { - await this.switchAdapter(expert.adapter); - const inferenceResult = await this.runInference(task.prompt, task); - - expertResult.output = inferenceResult.content; - expertResult.durationMs = inferenceResult.durationMs; - expertResult.tokPerSec = inferenceResult.tokPerSec; - expertResult.score = ArenaHarness.scoreOutput(inferenceResult.content, task, options); - } catch (err) { - expertResult.score = { score: 0, valid: false, errors: [err.message] }; - } - - results.push(expertResult); - } - - results.sort((a, b) => b.score.score - a.score.score); - - const winner = results[0]; - const summary = { - runId, - totalExperts: experts.length, - passedExperts: results.filter(r => r.score.valid).length, - winnerExpert: winner.expert.id, - winnerScore: winner.score.score, - winnerTokPerSec: winner.tokPerSec, - passRate: (results.filter(r => r.score.valid && r.score.score > 0.5).length / experts.length) * 100, - }; - - EventBus.emit('arena:adapter:complete', { runId, summary, winner: winner.expert.id }); - - return { winner, results, summary }; - }, - - mergeAdapters(adapters, strategy = 'lerp') { - if (adapters.length === 0) throw new Error('At least one adapter required'); - if (adapters.length === 1) return adapters[0].manifest; - - const first = adapters[0].manifest; - const mergedTensors = []; - const tensorsByName = new Map(); - - for (const { manifest, weight } of adapters) { - for (const tensor of manifest.tensors || []) { - if (!tensorsByName.has(tensor.name)) { - tensorsByName.set(tensor.name, []); - } - tensorsByName.get(tensor.name).push({ tensor, weight }); - } - } - - for (const [name, tensors] of tensorsByName) { - const shape = tensors[0].tensor.shape; - const totalElements = shape[0] * shape[1]; - const merged = new Float32Array(totalElements); - - if (strategy === 'add') { - for (const { tensor, weight } of tensors) { - const data = new Float32Array(tensor.data); - for (let i = 0; i < totalElements; i++) { - merged[i] += data[i] * weight; - } - } - } else if (strategy === 'lerp') { - const totalWeight = tensors.reduce((sum, t) => sum + t.weight, 0); - for (const { tensor, weight } of tensors) { - const data = new Float32Array(tensor.data); - const normalizedWeight = weight / totalWeight; - for (let i = 0; i < totalElements; i++) { - merged[i] += data[i] * normalizedWeight; - } - } - } - - mergedTensors.push({ name, shape, dtype: 'f32', data: Array.from(merged) }); - } - - return { - name: `merged-${strategy}-${adapters.length}adapters`, - rank: first.rank, - alpha: first.alpha, - tensors: mergedTensors, - }; - }, - - async getStatus() { - return { - available: true, - baseModel: baseModelId, - activeAdapter, - cachedAdapters: Array.from(adapterCache.keys()), - }; - }, - - async cleanup() { - adapterCache.clear(); - activeAdapter = null; - baseModelId = null; - }, - }; -}; - -describe('Doppler Arena Integration', () => { - let integration; - - beforeAll(async () => { - integration = createDopplerArenaIntegration(); - }); - - afterAll(async () => { - if (integration) { - await integration.cleanup(); - } - }); - - describe('Base Model Management', () => { - it('loads base model', async () => { - const loaded = await integration.loadBaseModel('gemma3-1b-q4'); - expect(loaded).toBe(true); - - const status = await integration.getStatus(); - expect(status.baseModel).toBe('gemma3-1b-q4'); - }); - }); - - describe('Adapter Management', () => { - it('loads and caches adapters', async () => { - const manifest = { - name: 'test-adapter', - rank: 8, - alpha: 16, - tensors: [{ name: 'layer0.q_proj.lora_a', shape: [8, 1024], data: new Array(8 * 1024).fill(0) }], - }; - - await integration.loadAdapter('adapter-1', manifest); - - const status = await integration.getStatus(); - expect(status.cachedAdapters).toContain('adapter-1'); - expect(status.activeAdapter).toBe('adapter-1'); - }); - - it('switches between adapters', async () => { - const manifest1 = { name: 'adapter-a', rank: 8, alpha: 16, tensors: [] }; - const manifest2 = { name: 'adapter-b', rank: 8, alpha: 16, tensors: [] }; - - await integration.loadAdapter('adapter-a', manifest1); - await integration.loadAdapter('adapter-b', manifest2); - - expect((await integration.getStatus()).activeAdapter).toBe('adapter-b'); - - await integration.switchAdapter('adapter-a'); - expect((await integration.getStatus()).activeAdapter).toBe('adapter-a'); - }); - - it('switches to base model (null adapter)', async () => { - await integration.switchAdapter(null); - expect((await integration.getStatus()).activeAdapter).toBeNull(); - }); - - it('throws on switching to non-existent adapter', async () => { - await expect(integration.switchAdapter('non-existent')).rejects.toThrow('Adapter not loaded'); - }); - }); - - describe('Inference with Adapters', () => { - it('runs inference with active adapter', async () => { - await integration.loadAdapter('code-adapter', { name: 'code-adapter', rank: 8, alpha: 16, tensors: [] }); - - const result = await integration.runInference('Write a function'); - - expect(result.content).toBeTruthy(); - expect(result.tokPerSec).toBeGreaterThan(0); - expect(result.adapter).toBe('code-adapter'); - }); - - it('runs inference without adapter (base model)', async () => { - await integration.switchAdapter(null); - - const result = await integration.runInference('Hello world'); - - expect(result.content).toBeTruthy(); - expect(result.adapter).toBeNull(); - }); - }); - - describe('Arena Expert Pool Competitions', () => { - it('runs competition between multiple adapters', async () => { - // Load adapters - await integration.loadAdapter('fast-adapter', { name: 'fast', rank: 4, alpha: 8, tensors: [] }); - await integration.loadAdapter('quality-adapter', { name: 'quality', rank: 16, alpha: 32, tensors: [] }); - - const experts = [ - integration.createExpert(null, { name: 'Base Model' }), - integration.createExpert('fast-adapter', { name: 'Fast' }), - integration.createExpert('quality-adapter', { name: 'Quality' }), - ]; - - const task = { - prompt: 'Solve this problem step by step', - maxTokens: 100, - }; - - const result = await integration.runAdapterCompetition(task, experts); - - expect(result.results.length).toBe(3); - expect(result.winner).toBeTruthy(); - expect(result.summary.passRate).toBeGreaterThanOrEqual(0); - expect(result.summary.totalExperts).toBe(3); - }); - - it('emits arena events', async () => { - const events = []; - integration.EventBus.on('arena:adapter:start', e => events.push({ type: 'start', ...e })); - integration.EventBus.on('arena:adapter:complete', e => events.push({ type: 'complete', ...e })); - - await integration.loadAdapter('test', { name: 'test', rank: 8, alpha: 16, tensors: [] }); - const experts = [integration.createExpert('test')]; - await integration.runAdapterCompetition({ prompt: 'Test' }, experts); - - expect(events.find(e => e.type === 'start')).toBeTruthy(); - expect(events.find(e => e.type === 'complete')).toBeTruthy(); - }); - - it('calculates passRate correctly', async () => { - const experts = [ - integration.createExpert(null), - integration.createExpert(null), - integration.createExpert(null), - ]; - - // Task that should produce high scores - const task = { prompt: 'Write a long detailed explanation' }; - const result = await integration.runAdapterCompetition(task, experts); - - expect(result.summary.passRate).toBeGreaterThanOrEqual(0); - expect(result.summary.passRate).toBeLessThanOrEqual(100); - }); - }); - - describe('Adapter Composition', () => { - const createTestAdapter = (name, value) => ({ - manifest: { - name, - rank: 8, - alpha: 16, - tensors: [{ - name: 'layer0.q_proj.lora_a', - shape: [2, 2], - data: [value, value, value, value], - }], - }, - weight: 1.0, - }); - - it('merges adapters with add strategy', () => { - const adapters = [ - { ...createTestAdapter('a', 1.0), weight: 1.0 }, - { ...createTestAdapter('b', 2.0), weight: 1.0 }, - ]; - - const merged = integration.mergeAdapters(adapters, 'add'); - - expect(merged.name).toContain('merged-add'); - expect(merged.tensors[0].data[0]).toBe(3.0); // 1.0 + 2.0 - }); - - it('merges adapters with lerp strategy', () => { - const adapters = [ - { ...createTestAdapter('a', 2.0), weight: 1.0 }, - { ...createTestAdapter('b', 4.0), weight: 1.0 }, - ]; - - const merged = integration.mergeAdapters(adapters, 'lerp'); - - expect(merged.name).toContain('merged-lerp'); - expect(merged.tensors[0].data[0]).toBe(3.0); // (2.0 + 4.0) / 2 - }); - - it('respects weights in lerp merge', () => { - const adapters = [ - { ...createTestAdapter('a', 0.0), weight: 1.0 }, - { ...createTestAdapter('b', 10.0), weight: 3.0 }, - ]; - - const merged = integration.mergeAdapters(adapters, 'lerp'); - - // (0.0 * 1.0 + 10.0 * 3.0) / (1.0 + 3.0) = 30.0 / 4.0 = 7.5 - expect(merged.tensors[0].data[0]).toBe(7.5); - }); - - it('handles single adapter (no merge)', () => { - const adapters = [createTestAdapter('single', 5.0)]; - - const result = integration.mergeAdapters(adapters); - - expect(result.tensors[0].data[0]).toBe(5.0); - }); - - it('throws on empty adapter list', () => { - expect(() => integration.mergeAdapters([])).toThrow('At least one adapter required'); - }); - }); -}); - -describe('Arena passRate with Adapter Switching', () => { - let integration; - - beforeAll(async () => { - integration = createDopplerArenaIntegration(); - await integration.loadBaseModel('gemma3-1b-q4'); - }); - - afterAll(async () => { - await integration.cleanup(); - }); - - it('tracks improvement with specialized adapters', async () => { - // Simulate: base model vs code-specialized adapter - await integration.loadAdapter('code-expert', { - name: 'code-expert', - rank: 16, - alpha: 32, - tensors: [], - }); - - const codingTask = { - prompt: 'Write a TypeScript function to sort an array', - maxTokens: 200, - }; - - const experts = [ - integration.createExpert(null, { name: 'Base' }), - integration.createExpert('code-expert', { name: 'Code Expert' }), - ]; - - const result = await integration.runAdapterCompetition(codingTask, experts); - - console.log(`[passRate Test] Base vs Code Expert:`); - console.log(` - Winner: ${result.winner.expert.name}`); - console.log(` - Pass Rate: ${result.summary.passRate}%`); - - // Both should produce valid output - expect(result.summary.passedExperts).toBe(2); - }); - - it('measures net improvement over iterations', async () => { - await integration.loadAdapter('iter-v1', { name: 'v1', rank: 8, alpha: 16, tensors: [] }); - await integration.loadAdapter('iter-v2', { name: 'v2', rank: 8, alpha: 16, tensors: [] }); - await integration.loadAdapter('iter-v3', { name: 'v3', rank: 8, alpha: 16, tensors: [] }); - - const task = { prompt: 'Improve this code', maxTokens: 100 }; - const iterations = []; - - for (const adapterId of [null, 'iter-v1', 'iter-v2', 'iter-v3']) { - const expert = integration.createExpert(adapterId); - const result = await integration.runAdapterCompetition(task, [expert]); - iterations.push({ - adapter: adapterId || 'base', - passRate: result.summary.passRate, - score: result.winner.score.score, - }); - } - - console.log('[RSI Improvement Test] Iterations:'); - iterations.forEach((iter, i) => { - console.log(` ${i + 1}. ${iter.adapter}: passRate=${iter.passRate}%, score=${iter.score.toFixed(2)}`); - }); - - // Verify we tracked all iterations - expect(iterations.length).toBe(4); - }); -}); diff --git a/tests/integration/long-session.test.js b/tests/integration/long-session.test.js index 3eb437d38..12f7324b0 100644 --- a/tests/integration/long-session.test.js +++ b/tests/integration/long-session.test.js @@ -3,7 +3,6 @@ * Tests memory system performance over 100+ turns without degradation. * * @see Blueprint 0x000068: Hierarchical Memory Architecture - * @see docs/TODO.md: Phase 4.4 Integration & Testing */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; diff --git a/tests/unit/confirmation-modal.test.js b/tests/unit/confirmation-modal.test.js deleted file mode 100644 index d99c0da4e..000000000 --- a/tests/unit/confirmation-modal.test.js +++ /dev/null @@ -1,319 +0,0 @@ -/** - * @fileoverview Unit tests for ConfirmationModal component - * Tests modal creation, user interactions, and promise resolution - */ - -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import ConfirmationModalModule from '../../ui/components/confirmation-modal.js'; - -describe('ConfirmationModal', () => { - let confirmationModal; - let mockUtils; - - beforeEach(() => { - document.body.innerHTML = ''; - - mockUtils = { - logger: { - debug: vi.fn(), - info: vi.fn(), - warn: vi.fn(), - error: vi.fn() - }, - escapeHtml: (text) => { - const div = document.createElement('div'); - div.textContent = text; - return div.innerHTML; - } - }; - - confirmationModal = ConfirmationModalModule.factory({ Utils: mockUtils }); - }); - - afterEach(() => { - document.body.innerHTML = ''; - vi.clearAllMocks(); - }); - - describe('metadata', () => { - it('should have correct module metadata', () => { - expect(ConfirmationModalModule.metadata.id).toBe('ConfirmationModal'); - expect(ConfirmationModalModule.metadata.type).toBe('ui'); - expect(ConfirmationModalModule.metadata.dependencies).toContain('Utils'); - }); - }); - - describe('confirm', () => { - it('should create modal overlay in DOM', async () => { - const promise = confirmationModal.confirm({ title: 'Test' }); - - expect(document.querySelector('.modal-overlay')).not.toBeNull(); - - // Clean up - document.querySelector('.modal-btn-cancel').click(); - await promise; - }); - - it('should display title', async () => { - const promise = confirmationModal.confirm({ title: 'My Title' }); - - const title = document.querySelector('.modal-title'); - expect(title.textContent).toBe('My Title'); - - document.querySelector('.modal-btn-cancel').click(); - await promise; - }); - - it('should display message', async () => { - const promise = confirmationModal.confirm({ message: 'Are you sure?' }); - - const message = document.querySelector('.modal-message'); - expect(message.textContent).toBe('Are you sure?'); - - document.querySelector('.modal-btn-cancel').click(); - await promise; - }); - - it('should display custom button text', async () => { - const promise = confirmationModal.confirm({ - confirmText: 'Yes, Delete', - cancelText: 'No, Keep' - }); - - const confirmBtn = document.querySelector('.modal-btn-confirm'); - const cancelBtn = document.querySelector('.modal-btn-cancel'); - - expect(confirmBtn.textContent).toBe('Yes, Delete'); - expect(cancelBtn.textContent).toBe('No, Keep'); - - document.querySelector('.modal-btn-cancel').click(); - await promise; - }); - - it('should display details when provided', async () => { - const promise = confirmationModal.confirm({ - title: 'Test', - details: 'Additional information here' - }); - - const details = document.querySelector('.modal-details'); - expect(details).not.toBeNull(); - expect(details.textContent).toBe('Additional information here'); - - document.querySelector('.modal-btn-cancel').click(); - await promise; - }); - - it('should not display details when not provided', async () => { - const promise = confirmationModal.confirm({ title: 'Test' }); - - const details = document.querySelector('.modal-details'); - expect(details).toBeNull(); - - document.querySelector('.modal-btn-cancel').click(); - await promise; - }); - - it('should add danger class when danger option is true', async () => { - const promise = confirmationModal.confirm({ danger: true }); - - const content = document.querySelector('.modal-content'); - expect(content.classList.contains('modal-danger')).toBe(true); - - const confirmBtn = document.querySelector('.modal-btn-confirm'); - expect(confirmBtn.classList.contains('border-error')).toBe(true); - - document.querySelector('.modal-btn-cancel').click(); - await promise; - }); - - it('should use default values when options not provided', async () => { - const promise = confirmationModal.confirm(); - - expect(document.querySelector('.modal-title').textContent).toBe('Confirm Action'); - expect(document.querySelector('.modal-message').textContent).toBe('Are you sure you want to proceed?'); - expect(document.querySelector('.modal-btn-confirm').textContent).toBe('Confirm'); - expect(document.querySelector('.modal-btn-cancel').textContent).toBe('Cancel'); - - document.querySelector('.modal-btn-cancel').click(); - await promise; - }); - - it('should escape HTML in title', async () => { - const promise = confirmationModal.confirm({ title: '' }); - - const title = document.querySelector('.modal-title'); - expect(title.innerHTML).not.toContain('