From c4f88a3d8344db0de4d8a36e4f4fbc7787affa43 Mon Sep 17 00:00:00 2001 From: Rickylabs Date: Sat, 18 Jul 2026 23:20:03 +0200 Subject: [PATCH 01/11] =?UTF-8?q?plan(frontend-contrib):=20seed=20run=20?= =?UTF-8?q?=E2=80=94=20frontend=20contribution=20layer=20design=20(generat?= =?UTF-8?q?or=20stage)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Research corpus, locked design (contracts/DX/discovery/host-runtime/scaffolding/doctrine), four worked consumer examples (dashboard, auth, ai, deploy), plan with owner forks F1-F8. Drafts only; STAGE-COMPLETE: generator — Sol adversarial + Kimi docs passes are supervisor-dispatched next. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_015fc7SaneZJ9mzrAhRW4tma --- .../context-pack.md | 53 ++++ .../design/canonical/00-overview.md | 120 +++++++++ .../design/canonical/01-contracts.md | 243 ++++++++++++++++++ .../design/canonical/02-authoring-dx.md | 160 ++++++++++++ .../canonical/03-discovery-and-registry.md | 120 +++++++++ .../design/canonical/04-host-runtime.md | 155 +++++++++++ .../canonical/05-scaffolding-and-cli.md | 82 ++++++ .../design/canonical/06-doctrine-fit.md | 70 +++++ .../design/examples/ai.md | 76 ++++++ .../design/examples/auth.md | 102 ++++++++ .../design/examples/dashboard.md | 84 ++++++ .../design/examples/deploy.md | 83 ++++++ .../runs/plan-frontend-contrib--seed/drift.md | 9 + .../plan-frontend-contrib--seed/kickoff.md | 60 +++++ .llm/runs/plan-frontend-contrib--seed/plan.md | 92 +++++++ .../plan-frontend-contrib--seed/research.md | 193 ++++++++++++++ .../plan-frontend-contrib--seed/supervisor.md | 34 +++ .../plan-frontend-contrib--seed/worklog.md | 57 ++++ 18 files changed, 1793 insertions(+) create mode 100644 .llm/runs/plan-frontend-contrib--seed/context-pack.md create mode 100644 .llm/runs/plan-frontend-contrib--seed/design/canonical/00-overview.md create mode 100644 .llm/runs/plan-frontend-contrib--seed/design/canonical/01-contracts.md create mode 100644 .llm/runs/plan-frontend-contrib--seed/design/canonical/02-authoring-dx.md create mode 100644 .llm/runs/plan-frontend-contrib--seed/design/canonical/03-discovery-and-registry.md create mode 100644 .llm/runs/plan-frontend-contrib--seed/design/canonical/04-host-runtime.md create mode 100644 .llm/runs/plan-frontend-contrib--seed/design/canonical/05-scaffolding-and-cli.md create mode 100644 .llm/runs/plan-frontend-contrib--seed/design/canonical/06-doctrine-fit.md create mode 100644 .llm/runs/plan-frontend-contrib--seed/design/examples/ai.md create mode 100644 .llm/runs/plan-frontend-contrib--seed/design/examples/auth.md create mode 100644 .llm/runs/plan-frontend-contrib--seed/design/examples/dashboard.md create mode 100644 .llm/runs/plan-frontend-contrib--seed/design/examples/deploy.md create mode 100644 .llm/runs/plan-frontend-contrib--seed/drift.md create mode 100644 .llm/runs/plan-frontend-contrib--seed/kickoff.md create mode 100644 .llm/runs/plan-frontend-contrib--seed/plan.md create mode 100644 .llm/runs/plan-frontend-contrib--seed/research.md create mode 100644 .llm/runs/plan-frontend-contrib--seed/supervisor.md create mode 100644 .llm/runs/plan-frontend-contrib--seed/worklog.md diff --git a/.llm/runs/plan-frontend-contrib--seed/context-pack.md b/.llm/runs/plan-frontend-contrib--seed/context-pack.md new file mode 100644 index 000000000..59b3e7b3a --- /dev/null +++ b/.llm/runs/plan-frontend-contrib--seed/context-pack.md @@ -0,0 +1,53 @@ +# Context Pack — plan-frontend-contrib--seed + +Resumable summary. Read with `supervisor.md` (identity + pipeline) and `drift.md`. + +## What this run is + +Planning-only seed (Fable 5 high) designing the **frontend contribution layer** for NetScript +plugins — the missing axis blocking dev dashboard, auth UI, ai surfaces, and the new deploy +plugin. Drafts only: no PRs/issues, no product code. Custom pipeline: this generator → Codex +GPT-5.6 Sol high adversarial pass → generator integrates → Kimi K3 public docs/API pass. All +downstream stages are supervisor-dispatched. + +## State: GENERATOR STAGE COMPLETE + +All deliverables written and committed on `plan/frontend-contrib`: + +- `research.md` — cited corpus (4-way Opus fan-out + supervisor verification). +- `plan.md` — D1–D14 locked decisions, F1–F8 owner forks, 3 implementation waves, gates, risks. +- `design/canonical/00-overview.md … 06-doctrine-fit.md` — the design. +- `design/examples/{dashboard,auth,ai,deploy}.md` — four worked consumers with author code. + +## The design in five lines + +1. `@netscript/plugin-frontend-core/contracts/v1`: FrontendManifest + route/island/zone/nav/theme + kinds; `defineFrontend()`; `definePluginPage()`; `PluginHostState`. +2. Core `@netscript/plugin` gains a pointer only (`.withFrontend({export,framework,contract})` + + manifest block). +3. Discovery = `netscript generate plugins` emitting `.netscript/generated/frontend.*` + (registry, islandSpecifiers, typed route refs, css); install-time `deno check` is the gate. +4. Hosts mount via `@netscript/fresh/plugins`: upstream `App.mountApp` sub-apps (plugin base + + host remap), `islandSpecifiers`/`registerIsland` builds, `/api/plugins//*` proxy, + `PluginZone`, nav→SidebarShell feed. `defineFreshApp({ frontend: registry })` is the one-liner. +5. Two delivery models: live (default) vs scaffolded starter via `AppTarget` (sign-in pages, + cloud-first seams). Dashboard's 7-kind family becomes an extension of this base. + +## Key verified facts (don't re-derive) + +- Upstream Fresh 2.3.3: `App.mountApp` (app.ts:357), `Builder.registerIsland` + (dev/builder.ts:157), `fresh({ islandSpecifiers })` (@fresh/plugin-vite 1.0.8 mod.ts:211-214). +- No `.tsx` under `plugins/`; no frontend axis in `PluginContributions`; fresh-ui is + copy-registry + `--ns-*` tokens; nav is hardcoded arrays; scaffolded apps have generated typed + route refs (`router.ts` + `createRouteReference`). +- Prior art: `dashboard-design--orchestrator/analysis/plugin-extension-architecture.md` (the + 437-line ratified dashboard design this generalizes); ai chat-route scaffolder is the one + existing plugin→app frontend precedent. + +## Next actions (supervisor) + +1. Dispatch Codex GPT-5.6 Sol high adversarial/collaborative pass over the run dir (enhance, not + ruin; findings file, generator integrates). +2. Resume this session (or a fresh Fable session with this pack) to integrate findings. +3. Dispatch Kimi K3 public-facing API + documentation story pass. +4. Owner review of F1–F8; then board filing is a separate decision (this run files nothing). diff --git a/.llm/runs/plan-frontend-contrib--seed/design/canonical/00-overview.md b/.llm/runs/plan-frontend-contrib--seed/design/canonical/00-overview.md new file mode 100644 index 000000000..2175e6c41 --- /dev/null +++ b/.llm/runs/plan-frontend-contrib--seed/design/canonical/00-overview.md @@ -0,0 +1,120 @@ +# Frontend Contribution Layer — Overview (canonical design, v1 draft) + +> **Draft — design document only. No GitHub mutations, no product code.** Produced by +> `plan-frontend-contrib--seed`; pending adversarial (GPT-5.6 Sol) and docs (Kimi K3) passes and +> owner ratification. + +## The one-paragraph story + +A NetScript plugin becomes a **full-stack unit**: the same `plugins/` package that today +contributes services, workers, schemas, and Aspire wiring gains a `frontend/` directory that +contributes **pages, islands, zone components, nav entries, and theme CSS** to any NetScript Fresh +host app. Contributions are declared as data (schema-first contracts in +`@netscript/plugin-frontend-core/contracts/v1`), discovered exactly like every other axis +(manifest pointer + `netscript generate plugins` emitting a type-checked registry into +`.netscript/generated/`), and mounted through thin wrappers over upstream Fresh 2.3 primitives +that already exist: `App.mountApp()` for routes, `islandSpecifiers`/`registerIsland` for islands, +`app.use()` for the data proxy. A plugin author writes ordinary Fresh code — the same +`routes/`-and-`islands/` idiom as an app — and one `defineFrontend()` export. Nothing is invented +that Fresh, the plugin SDK, or the fresh-ui token system doesn't already provide. + +## Why now (the four blocked consumers) + +| Consumer | Blocked need | Worked example | +| --- | --- | --- | +| Dev dashboard | plugin-contributed panels/consoles instead of hardcoded JSX | `../examples/dashboard.md` | +| Auth | sign-in flow, session widget, org/member management UI | `../examples/auth.md` | +| AI | chat + distributed AI surfaces mounted from the plugin, not hand-scaffolded | `../examples/ai.md` | +| Deploy (new) | cloud-specific consoles + cloud-first scaffold seams | `../examples/deploy.md` | + +## Design principles (locked) + +1. **DX is the contract.** The authoring experience is specified first (`02-authoring-dx.md`) and + every mechanism decision is scored against it. A plugin author writes Fresh code they already + know; the contribution layer is one declaration file, not a framework to learn. +2. **Contract first, mechanism second.** All shapes are Standard-Schema-validated types in a small + contract package (`@netscript/plugin-frontend-core`, Archetype 1). The registry, CLI, host + glue, and dashboard consume the contracts; none of them define vocabulary. +3. **Wrap, don't reinvent.** Every mount path is an existing upstream primitive: + `App.mountApp(path, app)` (jsr @fresh/core 2.3.3 `src/app.ts:357`), + `fresh({ islandSpecifiers })` (@fresh/plugin-vite 1.0.8 `src/mod.ts:211-214`), + `Builder.registerIsland` (core `src/dev/builder.ts:157`), `app.use(path, mw)`, + `createRouteReference` (`@netscript/fresh/route`), `--ns-*` semantic tokens. +4. **Generated registry, never runtime mutation.** Discovery is a build-time emission + (`.netscript/generated/frontend.*`), the same Walker→Extractor→Emitter idiom as every existing + axis (`packages/plugin/src/sdk/discovery/registry-emitter.ts`). `deno check` of the generated + workspace is the install-time compat gate: a type-broken contribution fails `plugin install` + with a real diagnostic. No `registerPanel()` globals (carried verdict from + `dashboard-design--orchestrator/analysis/plugin-extension-architecture.md` §2). +5. **Two delivery models, chosen per contribution, both first-class.** + - **Live** (default): the app imports the plugin's published frontend modules; updates ship + with the package version. For consoles, panels, widgets — surface the plugin *operates*. + - **Scaffolded starter** (opt-in): files are generated into the app and become app-owned — + the fresh-ui copy-registry / `chatRouteScaffolder` precedent + (`plugins/ai/src/adapter/resources/chat-route/chat-route.ts:33`). For surfaces the user is + *expected to customize* (a sign-in page, a cloud-optimized starter route). + The rule of thumb: **if the user will edit it, scaffold it; if the plugin owns it, serve it.** +6. **One base family, many hosts.** The user app and the dev dashboard are both hosts. The base + contract family (routes, islands, zones, nav, theme) is host-agnostic; the dashboard's richer + kinds (entity tabs, ⌘K actions, AI tools, home cards) are a *family extension* owned by the + future `plugin-dashboard-core`, not a sibling vocabulary. This generalizes — rather than + duplicates — the ratified dashboard architecture. +7. **Thinness preserved, axis first-class.** `@netscript/plugin` gains only a *pointer* + (`frontend: { export, framework, contract }`) — the same shape as `scaffolder.export` in the + installer manifest. The typed vocabulary lives in `plugin-frontend-core`. This resolves + OQ-12 (`plan-roadmap-expansion--seed/design/A-dashboard/open-questions.md:94-101`) by taking + both horns: first-class axis, zero core vocabulary. +8. **Trust is explicit but not theatrical.** An installed plugin already executes server-side + code; its frontend on the *app* surface is trusted (T0). The `requires` declaration + (ports/procedures) is kept for auditability, the doctor, and the dashboard's future T1/T2 + tiers — enforcement stays in the dashboard run's scope. + +## Architecture at a glance + +``` +plugins/crons/ + frontend/ + mod.ts ← defineFrontend(...) — the one declaration (02-authoring-dx.md) + routes/*.tsx ← ordinary Fresh route modules + islands/*.tsx ← ordinary Fresh islands (one component per file) + components/*.tsx ← SSR zone components + theme.css ← --ns-* overlays only + scaffold.plugin.json ← + "frontend": { "export": "./frontend", "framework": "fresh", "contract": "v1" } + deno.json ← + explicit exports for ./frontend and island/route modules (generator-maintained) + + │ netscript plugin install / netscript generate plugins + ▼ +app/.netscript/generated/ + frontend.registry.ts ← typed FrontendContributionRegistry (imports plugin manifests) + frontend.islands.ts ← const pluginIslandSpecifiers: string[] + frontend.routes.ts ← createRouteReference entries → routes.plugins.crons.calendar.href() + frontend.css ← aggregated theme overlays + + │ consumed by + ▼ +app/main.ts defineFreshApp({ name, frontend: fromGenerated(registry) }) ← mounts sub-apps, + nav, zones, proxy +app/vite.config.ts fresh({ islandSpecifiers: pluginIslandSpecifiers }) ← island builds +app/routes/… ← zone rendering +``` + +## Document map + +| Doc | Contents | +| --- | --- | +| `01-contracts.md` | The schema/type contract family — the normative surface | +| `02-authoring-dx.md` | The plugin-author experience — exact code, dev loop, conventions | +| `03-discovery-and-registry.md` | Manifest block, generation, install/uninstall, gates | +| `04-host-runtime.md` | Mount protocol, SSR/hydration, data access, nav, theming, isolation | +| `05-scaffolding-and-cli.md` | CLI verbs, scaffold template changes, starter resources, AppTarget | +| `06-doctrine-fit.md` | Archetypes, gates, debt, precedence, layering | +| `../examples/*.md` | The four worked consumer stories | + +## Explicitly out of scope for this layer (and where it lives) + +- Dashboard-only contribution kinds (panels-with-slots, entity tabs, ⌘K actions, AI tools, home + cards) — dashboard run, extending this base family. +- T1/T2 sandbox rendering (iframe + RPC bridge) — dashboard run ([stable] per prior design). +- Marketplace/verification — [stable], prior design §7. +- Non-Fresh frontend frameworks — the `framework` discriminator reserves the axis + (doctrine 07 axis table), nothing else is built. diff --git a/.llm/runs/plan-frontend-contrib--seed/design/canonical/01-contracts.md b/.llm/runs/plan-frontend-contrib--seed/design/canonical/01-contracts.md new file mode 100644 index 000000000..7e72b4d35 --- /dev/null +++ b/.llm/runs/plan-frontend-contrib--seed/design/canonical/01-contracts.md @@ -0,0 +1,243 @@ +# Contracts — `@netscript/plugin-frontend-core/contracts/v1` (draft) + +> **Draft — design document only.** Shapes are normative in intent, sketch in detail; field-level +> bikeshedding belongs to the adversarial + docs passes. Idiom follows +> `BasePluginContract` (`packages/plugin/src/contract-base/domain/base-contract.ts`): typed seams, +> Standard-Schema (zod) validation, re-exported — never redefined — by consumers. + +## Package + +`@netscript/plugin-frontend-core` — new **Archetype 1 (small contract)** package. Exports: + +```jsonc +// packages/plugin-frontend-core/deno.json (sketch) +{ + "name": "@netscript/plugin-frontend-core", + "exports": { + ".": "./mod.ts", // defineFrontend + re-export of contracts/v1 + "./contracts/v1": "./src/contracts/v1/mod.ts", + "./testing": "./src/testing/mod.ts" // renderContribution harness (phase 2) + } +} +``` + +Dependency posture: depends on nothing but `@netscript/contracts`-grade utilities and zod. +It must **not** depend on `fresh`, `@netscript/fresh`, or `@netscript/fresh-ui` — contributions +are data; only hosts touch the framework. (Component *references* are module specifiers, not +component values — see `ComponentRef` below. This is what keeps the contract package +framework-free and the registry statically emittable.) + +## The manifest — what a plugin's `./frontend` export IS + +```ts +// contracts/v1/manifest.ts (sketch) + +/** The value of a plugin's `./frontend` export — pure data, validated at build time. */ +export interface FrontendManifest { + /** Contract handshake. The ONLY compat gate (mirrors contributesTo in the dashboard family). */ + readonly contract: 'v1'; + /** Plugin id — must equal the owning plugin's manifest name (validated by the registry). */ + readonly plugin: string; + /** + * Preferred mount base for route contributions, e.g. '/crons'. The HOST may remap + * (04-host-runtime.md §2); collisions are a generate-time error, not a runtime surprise. + * Defaults to '/' + plugin id. + */ + readonly base?: string; + readonly routes?: readonly RouteContribution[]; + readonly islands?: readonly IslandContribution[]; + readonly zones?: readonly ZoneContribution[]; + readonly nav?: readonly NavContribution[]; + readonly theme?: readonly ThemeContribution[]; + /** Declared data needs — audit surface + doctor input + future T1/T2 enforcement. */ + readonly requires?: FrontendRequires; +} + +export interface FrontendRequires { + /** Core/host ports the contributions read (e.g. 'telemetry-query'). */ + readonly ports?: readonly string[]; + /** Own-contract oRPC procedures called through the plugin API proxy (04 §4). */ + readonly procedures?: readonly string[]; +} +``` + +## Component references + +```ts +// contracts/v1/refs.ts (sketch) + +/** + * A module specifier RELATIVE TO THE PLUGIN'S `frontend/` directory, e.g. + * './routes/calendar.tsx'. The registry emitter resolves it against the plugin's package name + * to a full importable specifier ('@acme/plugin-crons/frontend/routes/calendar'). + * Data, not code: manifests never hold component values. + */ +export type ComponentRef = `./${string}`; +``` + +## Contribution kinds (the base family) + +All kinds share: + +```ts +// contracts/v1/contribution.ts (sketch) +export interface FrontendContributionBase { + /** Stable id unique within the plugin: 'calendar', 'session-widget'. Full id = '/'. */ + readonly id: string; + readonly title?: string; + /** fresh-ui icon name (packages/fresh-ui primitives IconName). */ + readonly icon?: string; + /** + * Which host surfaces may mount this contribution. Default ['app']. + * 'dashboard' reserves the dev-dashboard host; its family extension adds dashboard-only kinds. + */ + readonly surfaces?: readonly ('app' | 'dashboard')[]; +} +``` + +### 1. `RouteContribution` — a page + +```ts +export interface RouteContribution extends FrontendContributionBase { + readonly kind: 'route'; + /** Path under the manifest base, Fresh syntax: '/calendar', '/schedules/:id'. */ + readonly path: string; + /** Fresh route module: default export page and/or `handler`, standard `define.*` idiom. */ + readonly module: ComponentRef; + /** Optional convenience: emit a NavContribution pointing here. */ + readonly nav?: Omit; +} +``` + +Route modules are **ordinary Fresh route modules** — `define.page`, `define.handlers`, layouts via +a plugin-local `_layout` (`04-host-runtime.md §2`). Lazy-loaded by the host +(`App.route(path, MaybeLazy)` upstream surface). + +### 2. `IslandContribution` — interactive component + +```ts +export interface IslandContribution extends FrontendContributionBase { + readonly kind: 'island'; + /** One island component per file (Fresh convention). All exports of the module hydrate. */ + readonly module: ComponentRef; // './islands/CronCalendar.tsx' +} +``` + +Islands are registered with the build (`islandSpecifiers` / `registerIsland`) so **plugin pages +and zone components simply import and render them** — the exact idiom app authors already use. +No wrapper, no props schema at v1: types flow through the import. + +### 3. `ZoneContribution` — SSR component into a host injection zone + +```ts +export interface ZoneContribution extends FrontendContributionBase { + readonly kind: 'zone'; + /** Published zone id (see AppZone below). Unknown zone = generate-time error. */ + readonly zone: AppZone | (string & {}); // dashboard family widens with its own zones + /** SSR component; may import the plugin's islands for interactivity. */ + readonly module: ComponentRef; + /** Deterministic ordering among contributions in the same zone (then by plugin id, then id). */ + readonly order?: number; +} + +/** v1 app-surface zones — a published, versioned enum. Additive growth = minor; removal = v2. */ +export type AppZone = + | 'app.topbar.end' // right side of the scaffolded topbar (_layout.tsx) + | 'app.dashboard.panels' // the scaffolded /dashboard panel grid + | 'app.home.cards' // the scaffolded index page card strip + | 'app.footer'; // global footer strip + +/** Props every zone component receives from the host. Deliberately minimal at v1. */ +export interface ZoneProps { + readonly url: URL; + /** Host state slice defined by PluginHostState (04-host-runtime.md §5). */ + readonly host: PluginHostState; +} +``` + +### 4. `NavContribution` — a typed nav entry + +```ts +export interface NavContribution extends FrontendContributionBase { + readonly kind: 'nav'; + readonly label: string; + /** Route contribution id (same plugin) or absolute href. */ + readonly route: string; + /** Host nav group. The scaffolded app has 'main' (topbar); hosts may define more. */ + readonly group?: string; + readonly order?: number; +} +``` + +Nav entries compile to the existing fresh-ui nav data model — `SidebarNavItem` / +`SidebarNavSection` (`packages/fresh-ui/registry/components/ui/sidebar-shell.tsx:8-32`) and the +topbar in the scaffolded `_layout.tsx.template` — and to typed route references +(`createRouteReference`, `@netscript/fresh/route`) so `routes.plugins.crons.calendar.href()` is +available app-wide. + +### 5. `ThemeContribution` — token-safe styling + +```ts +export interface ThemeContribution extends FrontendContributionBase { + readonly kind: 'theme'; + /** CSS files consuming ONLY the --ns-* semantic vocabulary (fresh-ui tokens). */ + readonly css: readonly ComponentRef[]; +} +``` + +v1 is a CSS overlay layer aggregated by the registry (`03-discovery-and-registry.md §3`); plugin +CSS is scoped under `[data-ns-plugin='']` (`04-host-runtime.md §6`). DTCG token-file merging +into `tokens:build` is an explicit **non-goal at v1** (owner fork F5, `../plan.md`). + +## The union and the registry shape + +```ts +export type FrontendContribution = + | RouteContribution | IslandContribution | ZoneContribution + | NavContribution | ThemeContribution; + +/** What .netscript/generated/frontend.registry.ts exports. */ +export interface FrontendContributionRegistry { + readonly contract: 'v1'; + readonly plugins: ReadonlyMap; +} + +/** Manifest after registry resolution: refs expanded to absolute specifiers, base finalized. */ +export interface ResolvedFrontendManifest extends FrontendManifest { + readonly resolvedBase: string; // after host remap + collision check + readonly specifierOf: (ref: ComponentRef) => string; // '@acme/plugin-crons/frontend/...' +} +``` + +## The pointer — the ONLY thing core learns + +```ts +// packages/plugin (additive; no vocabulary import) +export interface FrontendContributionRef { + readonly export: string; // './frontend' — a deno.json export of the plugin package + readonly framework: 'fresh'; // doctrine 07 axis discriminator; reserved for expansion + readonly contract: 'v1'; +} +// PluginContributions gains: readonly frontend?: FrontendContributionRef; +// PluginBuilder gains: .withFrontend(ref: FrontendContributionRef) +// PluginInstallerManifest (scaffold.plugin.json) gains the same block, parse-only: +// "frontend": { "export": "./frontend", "framework": "fresh", "contract": "v1" } +``` + +The pointer mirrors `scaffolder.export` (`packages/plugin/src/protocol/manifest.ts:28-34`): +readable at install/doctor time without executing plugin code. Bump +`PLUGIN_MANIFEST_SCHEMA_VERSION` additively. + +## Validation & versioning + +- `defineFrontend()` (see `02-authoring-dx.md`) validates against the zod schemas and freezes — + same posture as `definePlugin().build()` + (`packages/plugin/src/config/builders/plugin-builder.ts:282`). +- Cross-checks at registry-generation time (not runtime): plugin id matches; route ids referenced + by nav exist; zone ids known for the targeted surfaces; base collisions; duplicate contribution + ids (doctrine 07: duplicate rejection with a structured error naming both contributors). +- **Versioning**: `contract: 'v1'` + JSR-versioned subpath `contracts/v1`. Additive evolution + (new zones, new optional fields, new kinds) stays v1; breaking cuts `contracts/v2` with a host + support window — carried unchanged from the ratified dashboard design + (`plugin-extension-architecture.md` §4), including the quarantine-not-crash render rule and the + three-fact drift surfacing (package / contract / peer). diff --git a/.llm/runs/plan-frontend-contrib--seed/design/canonical/02-authoring-dx.md b/.llm/runs/plan-frontend-contrib--seed/design/canonical/02-authoring-dx.md new file mode 100644 index 000000000..c0b9fbdda --- /dev/null +++ b/.llm/runs/plan-frontend-contrib--seed/design/canonical/02-authoring-dx.md @@ -0,0 +1,160 @@ +# Authoring DX — what a plugin author actually writes (draft) + +> **Draft — design document only.** This document is the DX bar the implementation is held to. +> If a mechanism choice would make any snippet below longer or stranger, the mechanism is wrong. + +## The pitch, in one screen + +You already know how to write a Fresh app: `routes/`, `islands/`, components, `--ns-*` tokens. +A plugin frontend is **that, in a `frontend/` folder**, plus one declaration file: + +``` +plugins/crons/ + frontend/ + mod.ts + routes/ + calendar.tsx + schedules/[id].tsx + _layout.tsx + islands/ + CronCalendar.tsx + components/ + NextFiresCard.tsx + theme.css +``` + +```ts +// plugins/crons/frontend/mod.ts — the whole declaration +import { defineFrontend } from '@netscript/plugin-frontend-core'; + +export default defineFrontend({ + contract: 'v1', + plugin: 'crons', + base: '/crons', + routes: [ + { + kind: 'route', + id: 'calendar', + path: '/calendar', + module: './routes/calendar.tsx', + nav: { label: 'Cron calendar', icon: 'calendar', group: 'main' }, + }, + { kind: 'route', id: 'schedule-detail', path: '/schedules/:id', module: './routes/schedules/[id].tsx' }, + ], + islands: [{ kind: 'island', id: 'cron-calendar', module: './islands/CronCalendar.tsx' }], + zones: [ + { kind: 'zone', id: 'next-fires', zone: 'app.dashboard.panels', module: './components/NextFiresCard.tsx' }, + ], + theme: [{ kind: 'theme', id: 'theme', css: ['./theme.css'] }], + requires: { procedures: ['crons.list', 'crons.nextFires'] }, +}); +``` + +Register the axis on the plugin (one builder call + one manifest block): + +```ts +// plugins/crons/src/public/mod.ts — existing definePlugin chain +export const cronsPlugin = definePlugin('@acme/plugin-crons', VERSION) + .withType('utility') + .withService({ name: 'crons-api', entrypoint: './services/src/main.ts', port: 8093 }) + .withFrontend({ export: './frontend', framework: 'fresh', contract: 'v1' }) // ← new + .build(); +``` + +```jsonc +// plugins/crons/scaffold.plugin.json — additive block, parse-only discovery +{ "frontend": { "export": "./frontend", "framework": "fresh", "contract": "v1" } } +``` + +That's the complete authoring surface. Everything else — registry, islands build wiring, nav, +route refs, CSS aggregation, API proxy — is generated or host-side. + +## Writing the pages: it is just Fresh + +```tsx +// plugins/crons/frontend/routes/calendar.tsx — an ordinary Fresh route module +import { definePluginPage } from '@netscript/plugin-frontend-core'; +import { CronCalendar } from '../islands/CronCalendar.tsx'; +import { createCronsClient } from '@acme/plugin-crons/contracts/v1'; + +export default definePluginPage(async (ctx) => { + // Server-side: typed client against the plugin's own service (base URL from host config). + const crons = await createCronsClient(ctx.host.serviceUrl('crons-api')).crons.list(); + return ( +
+

Cron calendar

+ +
+ ); +}); +``` + +```tsx +// plugins/crons/frontend/islands/CronCalendar.tsx — an ordinary Fresh island +import { useSignal } from '@preact/signals'; +import { createCronsClient } from '@acme/plugin-crons/contracts/v1'; +import { pluginApi } from '@netscript/plugin-frontend-core/contracts/v1'; + +export function CronCalendar(props: { initial: readonly CronEntry[] }) { + const entries = useSignal(props.initial); + async function refresh() { + // Client-side: same typed client, same-origin proxy (/api/plugins/crons/…). + entries.value = await createCronsClient(pluginApi('crons')).crons.list(); + } + return ( /* fresh-ui-token-styled markup, --ns-* vars only */ ); +} +``` + +Key DX properties, each load-bearing: + +- **Islands are imported and rendered directly** — the registry feeds their specifiers to the + build (`islandSpecifiers`), so hydration Just Works. No island wrapper API, no registration + call in author code. +- **Types flow end-to-end** — pages import the plugin's own `contracts/v1` client; the island + props are ordinary TypeScript. Breaking a contract breaks `deno check` in the generated + workspace at install time, not the user's browser at runtime. +- **`definePluginPage`** is thin sugar over Fresh's `define.page` that types `ctx.host` + (`PluginHostState`: service URL resolution, session claims, base path) — the ONLY new concept a + page author meets. Plain `define.page` route modules also work; the sugar is optional. +- **Styling uses the app's tokens.** Only `--ns-*` semantic vars + (`packages/fresh-ui/tokens/semantic.tokens.json`) — the plugin UI is automatically correct in + light/dark and under any app theme override. Compiled fresh-ui *components* are not importable + (copy-registry, app-owned); plugin markup uses tokens + its own CSS + the fresh-ui runtime + helpers that ARE package exports (`Icon`, `DataGrid`, toast, `cn`, interactive namespaces). + +## The dev loop + +``` +netscript plugin new crons --with frontend # scaffolds the frontend/ skeleton above +cd plugins/crons && deno task dev # plugin dev = the HOST app runs with local-source + # plugin; vite watches plugins/** (watchPaths seam + # already exists in createNetScriptVitePlugin) +``` + +- Local-source plugins are workspace members; editing `frontend/routes/calendar.tsx` hot-reloads + the host app like any app file (vite `watchPaths` already includes workspace packages — + `packages/fresh/src/application/vite/README.md`). +- `netscript generate plugins` re-emits the registry; `plugin install`/`uninstall` runs it + automatically. Regeneration is idempotent (byte-identical skip — the `runtime-schemas` + precedent). +- `netscript plugin doctor` gains a `frontend` check: contract handshake, zone validity, module + refs resolvable, deno.json exports present. + +## Convention generator (phase 2 sugar — the contract stays explicit) + +`netscript generate frontend` (run inside the plugin) derives the `routes:`/`islands:` lists from +the file tree (Medusa's build-time convention, done the NetScript generated-file way) and +maintains the plugin's `deno.json` exports for `./frontend` and each island/route module — JSR +requires explicit exports, so the generator owns that bookkeeping, not the author. Authors who +prefer to write the manifest by hand simply do; the generated form is the same contract. + +## What an author CANNOT do (guardrails, stated as DX) + +- Import the app's copied `components/ui/*` — those are app-owned. Use tokens + package-safe + fresh-ui runtime exports. (The auth example shows the scaffolded-starter path when you *want* + the app to own and restyle a page — `../examples/auth.md`.) +- Claim routes outside the mount base — base remap/collisions are host decisions + (`04-host-runtime.md §2`). +- Mutate the registry at runtime — there is no API for it, by design. +- Ship raw hex/oklch colors — theme CSS is `--ns-*` vocabulary (lint-gated later; debt entry + until then). diff --git a/.llm/runs/plan-frontend-contrib--seed/design/canonical/03-discovery-and-registry.md b/.llm/runs/plan-frontend-contrib--seed/design/canonical/03-discovery-and-registry.md new file mode 100644 index 000000000..300f3c439 --- /dev/null +++ b/.llm/runs/plan-frontend-contrib--seed/design/canonical/03-discovery-and-registry.md @@ -0,0 +1,120 @@ +# Discovery & Registry — manifest to generated code (draft) + +> **Draft — design document only.** Mechanism follows the existing axis pipeline exactly; +> file names and shapes are sketches pending the adversarial pass. + +## 1. Discovery chain (all additive to existing machinery) + +``` +scaffold.plugin.json "frontend" block ← parse-only pointer (no plugin code executed) + │ netscript plugin install / generate plugins + ▼ +import plugin's './frontend' export ← FrontendManifest (validated data) + ▼ +emit .netscript/generated/frontend.* ← typed registry + islands + route refs + css + ▼ +deno check generated workspace ← EXISTING install gate; type-broken contribution + fails install with a real diagnostic +``` + +1. **Manifest block** — `PluginInstallerManifestSchema` gains an optional `frontend` object + (`packages/plugin/src/protocol/manifest.ts:204-216`); `parsePluginManifest` stays + execution-free. `capabilities` gains nothing: the block's presence IS the capability. +2. **Runtime manifest** — `PluginContributions.frontend?: FrontendContributionRef` + + `PluginBuilder.withFrontend()` + (`packages/plugin/src/config/domain/plugin-contributions.ts:12-38`, + `.../builders/plugin-builder.ts`). `CONTRIBUTION_AXES` gains `'frontend'` + (`packages/plugin/src/domain/constants.ts:16-40`) — the previously inert `PluginType + 'frontend'` value finally has machinery behind it. +3. **Emission** — `netscript generate plugins` + (`packages/cli/src/public/features/generate/plugins/generate-plugin-registries-command.ts`) + gains a frontend emitter alongside the per-axis registry emitter + (`packages/plugin/src/sdk/discovery/registry-emitter.ts`). Same AUTO-GENERATED / DO NOT EDIT + marker, same idempotency-by-construction, plus the byte-identical write skip + (`generate-runtime-schemas.ts:107-135` precedent). + +## 2. Generated files (the complete set) + +```ts +// .netscript/generated/frontend.registry.ts — AUTO-GENERATED, DO NOT EDIT +import cronsFrontend from '@acme/plugin-crons/frontend'; +import authFrontend from '@netscript/plugin-auth/frontend'; +import { buildFrontendRegistry } from '@netscript/plugin-frontend-core/contracts/v1'; + +export const frontendRegistry = buildFrontendRegistry([ + { manifest: cronsFrontend, specifierBase: '@acme/plugin-crons/frontend' }, + { manifest: authFrontend, specifierBase: '@netscript/plugin-auth/frontend' }, +], { + // host overrides from netscript.config (base remaps, disabled contributions) + overrides: hostOverrides, +}); +``` + +```ts +// .netscript/generated/frontend.islands.ts — consumed by vite.config.ts +export const pluginIslandSpecifiers: readonly string[] = [ + '@acme/plugin-crons/frontend/islands/CronCalendar', + '@netscript/plugin-auth/frontend/islands/SessionMenu', +]; +``` + +```ts +// .netscript/generated/frontend.routes.ts — typed route references (createRouteReference idiom, +// packages/cli/src/kernel/assets/app/router.ts.template precedent) +export const pluginRoutes = { + crons: { + calendar: createRouteReference('/crons/calendar', { id: 'plugins.crons.calendar', kind: 'page' }), + scheduleDetail: createRouteReference('/crons/schedules/:id', { id: 'plugins.crons.scheduleDetail', kind: 'page' }), + }, + auth: { /* … */ }, +} as const; +``` + +```css +/* .netscript/generated/frontend.css — aggregated theme overlays, imported by assets/design.css */ +@import '@acme/plugin-crons/frontend/theme.css' layer(ns-plugins); +``` + +(If dep-CSS imports prove brittle under vite, fallback: generate-time copy into +`.netscript/generated/frontend/.css` — decision deferred to implementation, both +mechanically equivalent. The fresh-ui CSS aggregator precedent is +`packages/cli/src/kernel/application/ui/registry.ts:110`.) + +Route-ref merging into the app's `router.ts` is **import-based, not file-mutation**: the +scaffolded `router.ts` template adds `...pluginRoutes` under a `plugins` key +(`05-scaffolding-and-cli.md §2`). Existing apps adopt via an idempotent `netscript generate +frontend-wiring` (same doc). + +## 3. Resolution rules (generate-time, structured errors) + +Deterministic order everywhere: plugins sorted by id, contributions by (`order`, plugin id, id) — +doctrine 07's deterministic-load-order law. + +| Check | Failure mode | +| --- | --- | +| plugin id in manifest ≠ owning plugin | error naming both | +| base collision (two plugins → same resolvedBase, after overrides) | error naming both + remap hint (`netscript.config` override) | +| nav.route references unknown route id | error | +| unknown zone id for a targeted surface | error listing published zones | +| duplicate contribution id within plugin | error | +| `contract` outside host window | **quarantine**: contribution excluded, registry carries a `quarantined` entry rendered as the fix-CLI panel state (prior design §4), never a host crash | +| module ref missing from plugin's deno.json exports | error + `netscript generate frontend` hint | + +## 4. Install / uninstall / update lifecycle + +- `netscript plugin install` — existing post-scaffold wiring + (`packages/cli/src/public/features/plugins/install/install-plugin.ts:145-192`) gains one step: + regenerate frontend emissions. Works identically for local-source and JSR modes (the registry + imports by package specifier either way; local-source resolves through the workspace). +- `netscript plugin uninstall` — regeneration drops the plugin's contributions (registry is + derived state). Scaffolded **starter** files remain, app-owned, per the copy-model contract. +- `netscript plugin update` — package version bump + regeneration; contract drift surfaces via + doctor + the quarantine state, remediation CLI printed. +- `netscript plugin doctor` — new `frontend` check via the existing `DoctorCheckSpec` seam + (`packages/plugin/src/adapter/contract.ts`): handshake, zones, refs, exports. + +## 5. Gates carried from the ratified dashboard design (unchanged) + +Install-time type-check (registry regeneration + `deno check`), doctor re-validation, render-time +quarantine-not-crash: `dashboard-design--orchestrator/analysis/plugin-extension-architecture.md` +§4 items 2a–2c apply verbatim to this layer. diff --git a/.llm/runs/plan-frontend-contrib--seed/design/canonical/04-host-runtime.md b/.llm/runs/plan-frontend-contrib--seed/design/canonical/04-host-runtime.md new file mode 100644 index 000000000..27667d7c9 --- /dev/null +++ b/.llm/runs/plan-frontend-contrib--seed/design/canonical/04-host-runtime.md @@ -0,0 +1,155 @@ +# Host Runtime — mounting, SSR/hydration, data, nav, theming, isolation (draft) + +> **Draft — design document only.** All mount paths wrap verified upstream primitives +> (research.md §3.1); the new code is glue in `@netscript/fresh` (new `./plugins` subpath). + +## 1. The host entry — one option on `defineFreshApp` + +```ts +// app/main.ts (scaffold template, after) +import { defineFreshApp } from '@netscript/fresh/server'; +import { frontendRegistry } from './.netscript/generated/frontend.registry.ts'; + +export const app = defineFreshApp({ + name: '{{appName}}', + frontend: frontendRegistry, // ← the entire host-side integration +}); +``` + +`defineFreshApp` (`packages/fresh/src/runtime/server/define-fresh-app.ts`) already exposes +`middleware[]` / `preConfigure` / `configure` seams; the `frontend` option is sugar that runs the +mount sequence below inside `configure`. Apps that need control call the pieces directly from +`@netscript/fresh/plugins`: + +```ts +import { mountPluginFrontends, pluginApiProxy, pluginNavSections } from '@netscript/fresh/plugins'; +``` + +## 2. Route mounting — sub-app per plugin + +For each plugin with route contributions the glue builds an upstream sub-`App` and mounts it: + +```ts +// inside mountPluginFrontends (sketch) +for (const [pluginId, m] of registry.plugins) { + const sub = new App(); + if (m.layoutModule) sub.layout('*', lazy(m.specifierOf(m.layoutModule))); // plugin _layout + for (const r of m.routes ?? []) sub.route(r.path, lazy(m.specifierOf(r.module))); + sub.use(pluginScopeMiddleware(pluginId)); // sets data-ns-plugin ctx + PluginHostState + app.mountApp(m.resolvedBase, sub); // upstream App.mountApp (core src/app.ts:357) +} +``` + +- **Base policy (locked, owner fork F2 records the alternative):** the plugin declares a + preferred `base` (default `/`); the **host** may remap per plugin in + `netscript.config` (`frontend: { basePaths: { crons: '/tools/crons' } }`); collisions are + generate-time errors. The dev-dashboard host auto-remaps everything under + `/plugins//…` — same registry, different policy. +- Plugin routes are lazy (`MaybeLazy` upstream) — code-splitting for free. +- A plugin `_layout` wraps only that plugin's pages; the app's root layout still applies + (upstream layout nesting semantics). + +## 3. Islands — build-time registration, both build paths + +- **Vite (scaffold default):** `vite.config.ts` template passes the generated list — + `fresh({ islandSpecifiers: [...pluginIslandSpecifiers] })` + (`@fresh/plugin-vite@1.0.8 src/mod.ts:56-63,211-214`; scaffold template currently calls bare + `fresh()` — `packages/cli/src/kernel/assets/app/vite.config.ts.template:44`). +- **Non-vite builder:** `withPluginIslands(builder, registry)` → + `builder.registerIsland(specifier)` (`@fresh/core@2.3.3 src/dev/builder.ts:157`). + +Because registration is by specifier, plugin code imports its islands like any component; SSR, +hydration, and props serialization are stock Fresh behavior. **Boundary rule (documented, not +new machinery):** island props must be serializable — the standard Fresh constraint. + +## 4. Data access — server direct, client via proxy + +- **Server (route handlers, zone components):** typed clients from the plugin's own + `contracts/v1` against the plugin service URL, resolved from host runtime config / Aspire + service discovery via `ctx.host.serviceUrl('')`. Services are an existing axis + (`ServiceContribution` — `packages/plugin/src/config/domain/service-contribution.ts`). +- **Client (islands):** same typed clients pointed at a same-origin proxy: + `pluginApiProxy(registry)` middleware mounted at `/api/plugins/:pluginId/*` + (upstream `app.use(path, mw)`), forwarding to the plugin service base URL. Precedent: the AI + chat island already streams through an app-local proxy path + (`plugins/ai/.../chat-route.stub.ts` → `/api/ai/chat-stream`; + `packages/fresh/src/runtime/ai/stream-proxy.ts`). The proxy forwards session/auth headers + set by app middleware; `requires.procedures` is the audit surface for what flows (enforcement + = dashboard-run scope). +- SSE/streaming passes through (the stream-proxy precedent covers eventIterator contracts). + +## 5. Host state — `PluginHostState` (the SSR/hydration contract seam) + +```ts +// contracts/v1 (sketch) — what plugin server code may assume about the host +export interface PluginHostState { + readonly plugin: string; // the mounted plugin id + readonly base: string; // resolvedBase, for building intra-plugin hrefs + readonly serviceUrl: (service: string) => string; + /** Session claims when the auth plugin is installed; null otherwise. Shape owned by plugin-auth-core. */ + readonly session: SessionClaims | null; +} +``` + +Populated by `pluginScopeMiddleware` from app state. Plugin pages must not reach past this seam +into app-specific `State` — the seam is what keeps contributions portable across hosts (app, +dashboard). `definePluginPage` types `ctx.host` so the constraint is ergonomic, not policed. + +## 6. Zones — SSR injection with provenance + +```tsx +// app markup (scaffold templates place these; apps may add/remove freely) +import { PluginZone } from '@netscript/fresh/plugins'; + +``` + +`PluginZone` renders every `ZoneContribution` targeting that zone (deterministic order), each +wrapped: + +```html +
…component…
+``` + +- SSR-only by default; interactivity comes from islands the zone component imports. +- A crashed zone component renders the quarantine card (fix CLI + provenance), never breaks the + page — the prior design's render-time rule applied at the component boundary + (Preact error boundary in `PluginZone`). +- Zone occupancy is inspectable: dev-mode overlay listing zones + occupants is a dashboard-run + deliverable; the data (registry) already supports it. + +## 7. Nav — feeding the existing shell, typed + +- `pluginNavSections(registry, { group })` → `readonly SidebarNavSection[]` — directly + spreadable into `SidebarShell.navigation` + (`packages/fresh-ui/registry/components/ui/sidebar-shell.tsx:24-36`) and the scaffolded topbar + (`_layout.tsx.template:33-74`), with `matchPrefix` set from the plugin base. +- Typed hrefs everywhere: `routes.plugins.crons.calendar.href()` via the generated + `frontend.routes.ts` merged in `router.ts` — no string paths in app code. + +## 8. Theming & style isolation + +- Plugin CSS is layered (`@layer ns-plugins`) below app CSS — apps always win specificity wars — + and scoped under `[data-ns-plugin='']` (the wrapper from §6 and the plugin layout from §2). +- Only `--ns-*` semantic vars (`packages/fresh-ui/tokens/semantic.tokens.json`, + `registry/theme/theme-bridge.css`): plugin UI inherits light/dark and any app theme override + with zero plugin-side work. `data-theme` switching (scaffold `_app.tsx` precedent) is free. +- Tailwind utilities inside plugin modules: plugin files are NOT in the app's Tailwind content + scan; v1 rule is **plain CSS + tokens (+ inline `class` on fresh-ui runtime components)** for + plugin-served UI. (Extending the app's Tailwind `content` globs to plugin packages is a + possible phase-2 nicety — flagged, not promised.) + +## 9. Isolation & failure containment (app surface = T0) + +- Installed plugins already run arbitrary server code — the app surface trusts them (research.md + §6.5). Containment here is about *blast radius of bugs*, not malice: lazy route mounting + (a broken page module fails on navigation, not boot), zone error boundaries, quarantined + contract drift, CSS layering. +- The `requires` declaration + provenance wrappers are the audit surface the dashboard's T1/T2 + tiers will later enforce (iframe + RPC bridge — dashboard run, [stable]). + +## 10. What is explicitly NOT built + +No runtime registration API; no plugin-to-plugin frontend imports (composition happens through +zones/nav, or through explicitly published packages); no client-side plugin router (Fresh +partials/`f-client-nav` already cover SPA-feel navigation); no shadow-DOM/web-component wrapper +(tokens + layers + scoping attributes suffice at T0). diff --git a/.llm/runs/plan-frontend-contrib--seed/design/canonical/05-scaffolding-and-cli.md b/.llm/runs/plan-frontend-contrib--seed/design/canonical/05-scaffolding-and-cli.md new file mode 100644 index 000000000..5e41f5b6c --- /dev/null +++ b/.llm/runs/plan-frontend-contrib--seed/design/canonical/05-scaffolding-and-cli.md @@ -0,0 +1,82 @@ +# Scaffolding & CLI — how contributions reach a project (draft) + +> **Draft — design document only.** Verb names follow existing CLI grammar +> (`packages/cli/src/public/features/root/public-command-tree.ts`); final naming is a docs-pass +> concern. + +## 1. CLI surface (additive) + +| Verb | Role | Precedent | +| --- | --- | --- | +| `netscript plugin install ` | unchanged UX; now also regenerates frontend emissions | existing post-install wiring (`install-plugin.ts:145-192`) | +| `netscript generate plugins` | now also emits `frontend.*` | per-axis emitter (`registry-emitter.ts`) | +| `netscript generate frontend-wiring` | idempotent adoption for EXISTING apps: vite `islandSpecifiers` feed, `router.ts` plugin-routes merge, `` hints, css import | `ui:init` + `generate runtime-schemas` idempotency | +| `netscript plugin new --with frontend` | plugin-side skeleton (`frontend/` tree + manifest block + exports) | `generate-plugin-mod.ts` template | +| `netscript generate frontend` (in plugin dir) | phase-2 convention generator: file tree → manifest lists + deno.json exports maintenance | `tokens:build` / assets-barrel generated-file idiom | +| `netscript plugin resource add --app ` | scaffolded-starter (copy-mode) resources into a chosen app | DDX-19 #432 one-generator-two-callers; `chatRouteScaffolder` | +| `netscript plugin doctor` | + `frontend` check | `DoctorCheckSpec` seam | +| `netscript ui *` | unchanged — fresh-ui copy-registry stays the app-owned component channel | `application/ui/registry.ts` | + +## 2. Scaffold template changes (new apps get the layer by default) + +All in `packages/cli/src/kernel/assets/app/`: + +- `main.ts.template` — `defineFreshApp({ name, frontend: frontendRegistry })` (import from + `./.netscript/generated/frontend.registry.ts`; generated file exists from day 0, empty + registry when no plugins contribute — zero conditional wiring). +- `vite.config.ts.template` — `fresh({ islandSpecifiers: [...pluginIslandSpecifiers] })`. +- `router.ts.template` — `plugins: pluginRoutes` merged into the exported `routes`/`appRoutes` + (typed hrefs for contributed pages). +- `routes/_layout.tsx.template` — topbar nav consumes `pluginNavSections(frontendRegistry, + { group: 'main' })` appended to the existing hardcoded entries; `` next to ``. +- `routes/dashboard.tsx.template` — `` under the existing + hardcoded panels (the hardcoded `services` array stays; contributed panels append). +- `routes/index.tsx.template` — ``. +- `assets/design.css` — `@import './.netscript/generated/frontend.css'`. + +The scaffolded app therefore demonstrates every zone and the nav feed **before** any plugin is +installed (empty states render nothing), and the first `plugin install` makes surface appear with +no app edits — the demo moment the DX story is built around. + +## 3. Scaffolded-starter resources (copy mode) — the `AppTarget` seam + +The second delivery model reuses the existing scaffolder engine end-to-end +(`PluginAdapter.toScaffold()`, `packages/plugin/src/adapter/contract.ts:305-309`; the live +precedent is `chatRouteScaffolder` emitting `routes/chat.tsx` into the app — +`plugins/ai/src/adapter/resources/chat-route/chat-route.ts:33`). What's added, carried verbatim +from the ratified prior design (§5): + +- `ScaffolderContext` gains `target?: AppTarget` — `{ app: string; framework: 'fresh' }` — + resolved against workspace members (`ensureWorkspaceMember` enumeration). +- `PluginResource` entries may declare `target: 'frontend'`; the CLI verb + `plugin resource add … --app ` selects the destination. +- Generated files land in the app's own `routes/`/`islands/` and are **app-owned from that + moment**: update flows report drift, never overwrite (the `updateUiRegistryItems` posture, + `application/ui/registry.ts:145-166`). +- Starter templates use `ItemScaffolder` typesafe codegen (#157 law) — no string templates. + +**Choosing the model** (the doc the author reads): serve it live when the plugin owns the +surface's evolution (consoles, panels, widgets); scaffold a starter when the user owns it +(sign-in pages, cloud-optimized starter routes). A plugin may ship both for the same feature — +auth does exactly this (`../examples/auth.md`). + +## 4. Generated-workspace layout in a scaffolded project (after install) + +``` +my-app/ + .netscript/generated/ + frontend.registry.ts frontend.islands.ts frontend.routes.ts frontend.css + services.registry.ts …existing axis registries… + routes/ islands/ components/ ← app-owned (incl. any scaffolded starters) + plugins/crons/ ← installed plugin (local-source/official copy modes) + netscript.config.* ← host overrides: basePaths, disabled contributions +``` + +## 5. E2E surface (gate wiring, for the plan) + +`scaffold.runtime` suite extension (design-time note, not run by this seed): install a +frontend-contributing plugin → assert generated emissions exist and type-check → boot app → +assert contributed route serves, nav entry present, zone renders, island hydrates (Playwright, +SCOPE-frontend gates), uninstall → assert clean disappearance. This is the acceptance shape for +phase 1 (`../plan.md` §Gates). diff --git a/.llm/runs/plan-frontend-contrib--seed/design/canonical/06-doctrine-fit.md b/.llm/runs/plan-frontend-contrib--seed/design/canonical/06-doctrine-fit.md new file mode 100644 index 000000000..744afc4c3 --- /dev/null +++ b/.llm/runs/plan-frontend-contrib--seed/design/canonical/06-doctrine-fit.md @@ -0,0 +1,70 @@ +# Doctrine Fit — archetypes, gates, layering, debt (draft) + +> **Draft — design document only.** + +## 1. Package/change map with archetypes + +| Surface | Change | Archetype | Overlay | +| --- | --- | --- | --- | +| `packages/plugin-frontend-core` | **new** — contracts/v1 + `defineFrontend` + `definePluginPage` sugar | ARCHETYPE-1 (small contract) | SCOPE-frontend | +| `packages/plugin` | additive: `FrontendContributionRef` pointer axis, builder method, manifest block, `CONTRIBUTION_AXES` | ARCHETYPE-4 (DSL/builder — existing) | — | +| `packages/fresh` | new `./plugins` subpath: mount glue, `PluginZone`, nav feed, api proxy, `withPluginIslands`; `defineFreshApp.frontend` option | ARCHETYPE-3 (runtime behavior — existing) | SCOPE-frontend | +| `packages/cli` | generate emissions, `frontend-wiring`, `plugin new --with frontend`, resource-add `--app`, doctor check, template updates | ARCHETYPE-6 (CLI/tooling — existing) | SCOPE-frontend | +| `plugins/ai`, `plugins/auth`, deploy plugin (future) | consumers: `frontend/` trees | ARCHETYPE-5 (plugin) | SCOPE-frontend | + +Implementation is a **cross-cutting wave** (like the plugin system itself); each slice lands under +its surface's archetype gate set (`gates/archetype-gate-matrix.md`) — F-1…F-19 per column, plus +SCOPE-frontend browser/route/state gates for everything user-visible. + +## 2. Layering laws honored (doctrine 07) + +- **Thinness**: `@netscript/plugin` learns a pointer, no vocabulary (§0 of the ratified dashboard + design; OQ-12 resolution recorded in `01-contracts.md`). `plugin-frontend-core` depends on + neither `fresh` nor `fresh-ui`. +- **Registration over inheritance** (`07-composition-and-extension.md:114-134`): contributions + are data registered against an axis; no cross-package subclassing anywhere in the design. +- **Deterministic order + duplicate rejection** (`:135-153`): resolution rules in + `03-discovery-and-registry.md §3`. +- **Extension-axis discipline** (`:101-112`): typed identifier (`framework: 'fresh'`), factory + (registry build), registration mechanism (manifest pointer) — the axis table's "Frontend + framework" row becomes real. +- **R-COMP-EXT-MANIFEST** (`:255-289`): `@netscript/fresh` and `packages/plugin` extension-points + manifests gain the new registries; `plugin-frontend-core` documents its single axis in its + `mod.ts` docs (one-axis package, manifest optional per the rule's 2+ threshold). +- **Wrap, don't reinvent**: every mount primitive is upstream Fresh (research.md §3.1); + tokens/nav/route-refs are existing NetScript surfaces. + +## 3. Public-surface / JSR posture + +- `plugin-frontend-core` publishes `.`, `./contracts/v1`, `./testing` — versioned-subpath + contract evolution (v2 additive path), `deno doc --lint` publish bar, isolatedDeclarations + discipline like sibling `*-core` packages. +- Plugin packages publishing frontends must export `./frontend` + island/route modules + explicitly (JSR has no wildcard exports) — maintained by the phase-2 generator; documented as + the author-facing rule until then. +- Text-import doctrine applies: any starter templates ship as string constants, never text + imports (`jsr-text-import-lineage` standing doctrine). + +## 4. Known debt to record on implementation (candidates, not yet filed) + +| Candidate | Why deferred | +| --- | --- | +| `--ns-*`-only CSS lint for plugin theme files | needs a lint rule; v1 is documentation + review | +| Tailwind content-scan extension to plugin packages | vite/tailwind config reach; plain CSS suffices v1 | +| DTCG token-file merge from plugins into `tokens:build` | owner fork F5 deferred | +| Island props serializability static check | upstream Fresh constraint; no static gate exists | +| Zone occupancy caps / conflicts UI | dashboard-run inspector covers observability first | + +## 5. Precedence honored (what this design reuses rather than invents) + +| Reused | Source | +| --- | --- | +| 7-member dashboard family, zones, handshake, quarantine, trust tiers | `dashboard-design--orchestrator/analysis/plugin-extension-architecture.md` (generalized, not replaced) | +| Per-axis generated registry + markers + idempotency | `packages/plugin/src/sdk/discovery/registry-emitter.ts`, `generate-runtime-schemas.ts` | +| Manifest pointer idiom | `scaffolder.export` (`packages/plugin/src/protocol/manifest.ts`) | +| Copy-registry ownership + drift semantics | `packages/cli/src/kernel/application/ui/registry.ts` | +| Plugin→app file generation | `plugins/ai/.../chat-route/` + `PluginAdapter.toScaffold()` | +| Typed route references | `@netscript/fresh/route` + `router.ts.template` | +| Token system | `packages/fresh-ui/tokens/*` + `theme-bridge.css` | +| Nav data model | `sidebar-shell.tsx` `SidebarNavItem`/`SidebarNavSection` | +| External-model teardown (TanStack/Nuxt/Directus/Medusa) | prior design Appendix A | diff --git a/.llm/runs/plan-frontend-contrib--seed/design/examples/ai.md b/.llm/runs/plan-frontend-contrib--seed/design/examples/ai.md new file mode 100644 index 000000000..2012e07ba --- /dev/null +++ b/.llm/runs/plan-frontend-contrib--seed/design/examples/ai.md @@ -0,0 +1,76 @@ +# Worked example — AI surfaces (draft) + +> **Draft — design document only.** Upgrades the one existing plugin→app frontend precedent +> (scaffold-only chat route) into the two-model story, without new chat machinery. + +## Today + +- Backend: oRPC `chat` (SSE eventIterator) + `models` + (`packages/plugin-ai-core/src/contracts/v1/ai.contract.ts:317-391`). +- Runtime plane exists: `@netscript/fresh/ai` — `createNetScriptChatConnection`, + `projectChatSnapshot`, durable sessions, stream proxy + (`packages/fresh/src/runtime/ai/mod.ts`). +- UI components exist: fresh-ui chat family (`message`, `prompt-input`, `model-selector`, + `tool-call-card`, `src/ai/render-ui.tsx`). +- Delivery today is **scaffold-only**: `chatRouteScaffolder` emits `routes/chat.tsx` + a + `ChatIsland` into the app (`plugins/ai/src/adapter/resources/chat-route/chat-route.ts:33`, + `chat-route.stub.ts`). No live surface, no nav, no reusable widget. + +## Contribution declaration + +```ts +// plugins/ai/frontend/mod.ts +import { defineFrontend } from '@netscript/plugin-frontend-core'; + +export default defineFrontend({ + contract: 'v1', + plugin: 'ai', + base: '/ai', + routes: [ + // LIVE: a ready-to-use chat console — durable sessions, model picker, tool calls. + { + kind: 'route', + id: 'chat', + path: '/chat', + module: './routes/chat.tsx', + nav: { label: 'Assistant', icon: 'sparkles', group: 'main' }, + }, + { kind: 'route', id: 'sessions', path: '/sessions', module: './routes/sessions.tsx' }, + ], + islands: [ + { kind: 'island', id: 'chat', module: './islands/Chat.tsx' }, + { kind: 'island', id: 'assist-launcher', module: './islands/AssistLauncher.tsx' }, + ], + zones: [ + // A compact "ask the assistant" launcher in the topbar; opens the chat as an overlay. + { kind: 'zone', id: 'assist', zone: 'app.topbar.end', module: './components/AssistButton.tsx' }, + ], + requires: { ports: ['ai-stream-proxy'], procedures: ['ai.chat', 'ai.models'] }, +}); +``` + +The live `Chat.tsx` island is the productionized `chat-route.stub.ts` content moved INTO the +plugin: `createNetScriptChatConnection` against the plugin API proxy +(`/api/plugins/ai/chat` — SSE passes through, `04-host-runtime.md §4`), fresh-ui token styling. + +## The starter stays — repositioned, not removed + +`netscript plugin resource add ai chat-page --app .` keeps emitting an app-owned `chat.tsx` for +products building a bespoke chat UX (the current scaffolder, now targeted via `AppTarget`). +Docs frame the choice: **start live, eject to starter when you need to own the pixels.** The +eject path is honest: the starter is the same component source, scaffolded once, drift-tracked. + +## Distributed AI (the owner's Axis-5 direction, mapped to this layer) + +"Not one chat pane" (dashboard brief Axis 5) decomposes cleanly: + +| Axis-5 need | Where it lands | +| --- | --- | +| Reusable assist affordances in arbitrary app surfaces | `AssistLauncher` island — any app page or plugin zone component can import & render it (islands are package exports) | +| Contextual "explain this / fix this" on other plugins' surfaces | those plugins import `@netscript/plugin-ai/frontend/islands/AssistLauncher` and pass context — plugin-to-plugin composition through **published exports**, the sanctioned path (`04-host-runtime.md §10`) | +| Procedures-as-agent-tools (`DashboardAiToolContribution`) | **dashboard family extension**, not this base layer — the dashboard host's tool registry consumes it (`../canonical/00-overview.md` out-of-scope table) | + +## After `netscript plugin install @netscript/plugin-ai` + +`/ai/chat` serves with durable sessions; "Assistant" in nav; assist button in the topbar; +`routes.plugins.ai.chat.href()` typed everywhere. Zero app edits. diff --git a/.llm/runs/plan-frontend-contrib--seed/design/examples/auth.md b/.llm/runs/plan-frontend-contrib--seed/design/examples/auth.md new file mode 100644 index 000000000..7f604f52a --- /dev/null +++ b/.llm/runs/plan-frontend-contrib--seed/design/examples/auth.md @@ -0,0 +1,102 @@ +# Worked example — Auth UI (draft) + +> **Draft — design document only.** Demonstrates BOTH delivery models on one plugin. + +## Today + +Mature backend, zero frontend: oRPC `signin/callback/signout/session/me` +(`packages/plugin-auth-core/src/contracts/v1/auth.contract.ts:437-459`); better-auth adapter with +`organization/admin` plugins (`packages/auth-better-auth/src/better-auth.ts:56,132-138`); workos +(`organizationId`) and kv-oauth adapters. No `.tsx` in any auth package. + +## Contribution declaration + +```ts +// plugins/auth/frontend/mod.ts +import { defineFrontend } from '@netscript/plugin-frontend-core'; + +export default defineFrontend({ + contract: 'v1', + plugin: 'auth', + base: '/auth', + routes: [ + // LIVE: the plugin owns account/org management — ships and evolves with the package. + { kind: 'route', id: 'account', path: '/account', module: './routes/account.tsx' }, + { + kind: 'route', + id: 'org', + path: '/org', + module: './routes/org.tsx', + nav: { label: 'Organization', icon: 'users', group: 'main' }, + }, + { kind: 'route', id: 'org-members', path: '/org/members', module: './routes/org/members.tsx' }, + ], + islands: [ + { kind: 'island', id: 'session-menu', module: './islands/SessionMenu.tsx' }, + { kind: 'island', id: 'member-table', module: './islands/MemberTable.tsx' }, + ], + zones: [ + // Session widget appears in the app topbar the moment auth is installed. + { kind: 'zone', id: 'session', zone: 'app.topbar.end', module: './components/SessionWidget.tsx' }, + ], + requires: { procedures: ['auth.session', 'auth.me', 'auth.signout', 'auth.org.*'] }, +}); +``` + +```ts +// plugins/auth/src/public/mod.ts — one added builder call +.withFrontend({ export: './frontend', framework: 'fresh', contract: 'v1' }) +``` + +## The sign-in page is a SCAFFOLDED STARTER, not a live route + +Sign-in is the page every product restyles — brand, copy, layout. Serving it live would fight the +user; so auth ships it as a starter resource through the existing scaffolder engine +(`05-scaffolding-and-cli.md §3`): + +``` +netscript plugin resource add auth signin --app . + → routes/auth/signin.tsx (app-owned from now on; posts to the auth contract) + → routes/auth/callback.tsx (provider redirect landing — kv-oauth handleCallback / better-auth) +``` + +The generated `signin.tsx` is ordinary app code: fresh-ui-token styling, typed +`createAuthClient(...)` calls against `signin`/`callback` — the user edits it freely; `plugin +update` reports drift, never overwrites (`ui registry` posture). + +**Rule of thumb applied:** user-owned surface (sign-in) → scaffolded; plugin-owned surface +(org/member console, session widget) → live. + +## What a page looks like (live org console) + +```tsx +// plugins/auth/frontend/routes/org/members.tsx +import { definePluginPage } from '@netscript/plugin-frontend-core'; +import { createAuthClient } from '@netscript/plugin-auth/contracts/v1'; +import { MemberTable } from '../../islands/MemberTable.tsx'; + +export default definePluginPage(async (ctx) => { + if (!ctx.host.session) return ctx.redirect('/auth/signin'); // starter page, if scaffolded + const client = createAuthClient(ctx.host.serviceUrl('auth-api')); + const members = await client.org.members({ org: ctx.host.session.orgId }); + return ; // island refreshes via pluginApi('auth') proxy +}); +``` + +`SessionWidget` (zone component) SSRs the signed-in state from `ctx.host.session` (populated by +the auth middleware the plugin's service wiring already provides) and imports `SessionMenu` +island for the dropdown/sign-out action. + +## What appears in the app after `netscript plugin install @netscript/plugin-auth` + +- `/auth/org`, `/auth/org/members`, `/auth/account` serve (typed refs: + `routes.plugins.auth.org.href()`). +- "Organization" appears in the topbar nav; the session widget appears at `app.topbar.end`. +- Nothing app-owned changed. Sign-in appears only when the user opts into the starter. + +## Adapter note + +Org/member management renders capability-degraded per backend: better-auth `organization` plugin +and workos organizations expose management procedures; kv-oauth has none → the org routes render +the empty-capability state (procedure presence is discoverable through the auth contract, not +sniffed in UI code). This keeps one frontend across all three adapters — harmonized-ports law. diff --git a/.llm/runs/plan-frontend-contrib--seed/design/examples/dashboard.md b/.llm/runs/plan-frontend-contrib--seed/design/examples/dashboard.md new file mode 100644 index 000000000..afc5fd0f9 --- /dev/null +++ b/.llm/runs/plan-frontend-contrib--seed/design/examples/dashboard.md @@ -0,0 +1,84 @@ +# Worked example — Dev-dashboard panels (draft) + +> **Draft — design document only.** Shows (a) how the base family already carries useful panels +> in the scaffolded app today, and (b) how the ratified dashboard architecture becomes a family +> extension of this layer instead of a sibling. + +## Today + +No dashboard package exists; the scaffolded `/dashboard` route renders a hardcoded `services` +array (`packages/cli/src/kernel/assets/app/routes/dashboard.tsx.template`), nav is static arrays +(`DESIGN_NAVIGATION`; `_layout.tsx.template:33-74`). The full dashboard contribution family (7 +kinds) is designed but unbuilt +(`dashboard-design--orchestrator/analysis/plugin-extension-architecture.md`). + +## Step 1 (this layer, phase 1-2): plugin panels in the SCAFFOLDED app + +The workers plugin surfaces its queue health on the app's own `/dashboard` page — no dashboard +host needed: + +```ts +// plugins/workers/frontend/mod.ts +export default defineFrontend({ + contract: 'v1', + plugin: 'workers', + base: '/workers', + routes: [{ + kind: 'route', id: 'console', path: '/', module: './routes/console.tsx', + nav: { label: 'Workers', icon: 'activity', group: 'main' }, + }], + islands: [{ kind: 'island', id: 'queue-live', module: './islands/QueueLive.tsx' }], + zones: [{ + kind: 'zone', id: 'queue-health', zone: 'app.dashboard.panels', + module: './components/QueueHealthPanel.tsx', order: 10, + }], + requires: { procedures: ['workers.jobs.stats', 'workers.dlq.depth'] }, +}); +``` + +```tsx +// plugins/workers/frontend/components/QueueHealthPanel.tsx — SSR zone component +import type { ZoneProps } from '@netscript/plugin-frontend-core/contracts/v1'; +import { createWorkersClient } from '@netscript/plugin-workers-core/contracts/v1'; +import { QueueLive } from '../islands/QueueLive.tsx'; + +export default async function QueueHealthPanel({ host }: ZoneProps) { + const stats = await createWorkersClient(host.serviceUrl('workers-api')).jobs.stats(); + return ( +
{/* --ns-* styled card */} +

Job queues

+ {/* hydrates; polls via pluginApi('workers') */} + Open workers console → +
+ ); +} +``` + +Install workers → the scaffolded dashboard grows a live queue-health panel and the nav gains +"Workers", with `/workers` serving a full console page. The four first-party plugins +(workers/sagas/triggers/streams) each ship one zone panel + one console route — **the dogfood +proof** (#427's acceptance idea, relocated to the app surface). + +## Step 2 (dashboard run): the dashboard host consumes the same registry + +The future dev dashboard is a NetScript Fresh app whose host policy differs, not a new mechanism: + +- mounts every plugin's routes under `/plugins//…` (host base remap, + `04-host-runtime.md §2`); +- defines **additional zones** (`dashboard.home.*`, `entity..detail.sidebar`, …) — the + `ZoneContribution.zone` type is open to host-published zone sets by design; +- targets contributions with `surfaces: ['dashboard']` where a panel is dashboard-only. + +## Step 3 (dashboard run): the family extension + +`plugin-dashboard-core/contracts/v1` re-exports this base family and **adds** dashboard-only +kinds — panels-with-slots/setup bindings, `entity-tab`, `action` (⌘K + `cliEquivalent` confirm), +`ai-tool`, `home-card` — exactly the seven-member union already ratified, now defined as +`DashboardContribution = FrontendContribution | DashboardPanelContribution | …`. The dashboard's +manifest block, registry emission, trust tiers (T1/T2 iframe sandbox), and injection-zone +inspector proceed per the prior design, unchanged, on top of this layer's discovery pipeline — +one emission, one handshake, one doctor check family. + +**What this run hands the dashboard run:** the discovery pipeline, the mount glue, zones, nav, +route refs, and four dogfood panels — so the dashboard epic starts at "define richer kinds and +the host app", not "invent discovery". diff --git a/.llm/runs/plan-frontend-contrib--seed/design/examples/deploy.md b/.llm/runs/plan-frontend-contrib--seed/design/examples/deploy.md new file mode 100644 index 000000000..489b9ea31 --- /dev/null +++ b/.llm/runs/plan-frontend-contrib--seed/design/examples/deploy.md @@ -0,0 +1,83 @@ +# Worked example — Deploy plugin (draft) + +> **Draft — design document only.** The freshest consumer: branch `plan/deploy-plugin` (stage-A +> bootstrap) redefines deploy as a plugin so it can "contribute to every layers (even frontend +> soon)" — owner-ratified intent quoted in its kickoff. This example shows the two cloud seams +> that intent implies, expressed against this layer's API. It is a design input to the deploy +> seed run, not a commitment on its behalf. + +## Assumed shape (from ARCHETYPE-7 + the deploy-plugin direction) + +`plugin-deploy` (core) + per-target adapter packages (`deploy-cloudflare`, `deploy-aws`, +`deploy-vercel`) behind the deploy-target port with the uniform op set +`plan/emit/up/down/status/logs` (`.llm/harness/archetypes/ARCHETYPE-7-deploy-target-adapter.md`). + +## Seam 1 — LIVE: the deploy console + +The core deploy plugin contributes one console; **target adapters contribute panels into it +without touching core** — the same composition trick the app dashboard uses, via a +plugin-published zone pattern: + +```ts +// plugins/deploy/frontend/mod.ts (core) +export default defineFrontend({ + contract: 'v1', + plugin: 'deploy', + base: '/deploy', + routes: [{ + kind: 'route', id: 'console', path: '/', module: './routes/console.tsx', + nav: { label: 'Deploy', icon: 'rocket', group: 'main' }, + }, { + kind: 'route', id: 'environment', path: '/env/:env', module: './routes/env.tsx', + }], + islands: [ + { kind: 'island', id: 'status-live', module: './islands/StatusLive.tsx' }, + { kind: 'island', id: 'logs-tail', module: './islands/LogsTail.tsx' }, + ], + zones: [{ + kind: 'zone', id: 'deploy-status', zone: 'app.dashboard.panels', + module: './components/DeployStatusCard.tsx', order: 20, + }], + requires: { procedures: ['deploy.status', 'deploy.logs', 'deploy.plan'] }, +}); +``` + +The console page renders per-target sections from the deploy-target registry (a backend +registry, ARCHETYPE-7); `StatusLive`/`LogsTail` stream `status`/`logs` through the plugin API +proxy. Mutating ops (`up`/`down`) render as confirm-gated actions **printing the exact CLI +equivalent** (`netscript deploy cloudflare up --env staging`) — the NetScript signature carried +from the dashboard family's action design; on the app surface this is a plugin-internal pattern, +formalized as an `ActionContribution` kind only in the dashboard family. + +**Cloud-specific console surface** comes from the adapter packages as *published islands* the +core console composes (plugin-to-plugin composition through explicit exports — +`04-host-runtime.md §10`): `deploy-cloudflare` exports a `WorkersKvBrowser` island, `deploy-aws` +a `QueueDepthPanel`. Core's console imports lazily per registered target; an absent adapter means +an absent section. If adapters later become standalone plugins, each ships its own +`frontend/mod.ts` and the composition moves to zones — both paths are supported by the layer. + +## Seam 2 — SCAFFOLDED STARTER: cloud-optimized frontend seams + +The owner's scaffolding intent ("a cloudflare optimized project that ships seams that already +are cloudflare first — workers, durable objects, KV") is the `AppTarget` copy model, verb-for-verb +(`05-scaffolding-and-cli.md §3`): + +``` +netscript plugin resource add deploy cloudflare-edge --app . + → routes/api/edge/[...path].ts (Workers-first handler seam, app-owned) + → lib/deploy/kv.ts (KV binding wrapper typed against the adapter port) + → lib/deploy/durable.ts (DO session seam) + → netscript.config patch (deploy target defaults: cloudflare) +``` + +Same story per target (`aws-lambda-edge`, `vercel-functions` starters). These are **starters** — +cloud-first code the user owns and evolves — while `deploy.status`/`logs`/`up` stay live in the +console. The one-generator-two-callers law holds: the dashboard's future "Add to app" flow calls +the same scaffolder with the same `AppTarget`. + +## What this hands the deploy seed run + +A concrete answer to its kickoff §3 ("what the deploy plugin contributes to EVERY layer"): the +frontend row of that table is `defineFrontend` (console + status panels + per-adapter islands) +plus `AppTarget` starter resources (cloud-first seams) — no deploy-specific frontend machinery +to invent. diff --git a/.llm/runs/plan-frontend-contrib--seed/drift.md b/.llm/runs/plan-frontend-contrib--seed/drift.md new file mode 100644 index 000000000..c7f36a023 --- /dev/null +++ b/.llm/runs/plan-frontend-contrib--seed/drift.md @@ -0,0 +1,9 @@ +# Drift Log — plan-frontend-contrib--seed + +Append-only. Severity: `minor` | `significant` | `architectural`. + +| # | Date | Severity | Drift | Disposition | +| --- | --- | --- | --- | --- | +| 1 | 2026-07-18 | minor | Kickoff stop-lines forbid opening the seed-run draft PR that `workflow/seed-run.md` stage A prescribes. Commit trail is direct commits on `plan/frontend-contrib` (explicit refspec push); PR opening is deferred to the supervisor. | Recorded in `supervisor.md` § overrides; run proceeds PR-less. | +| 2 | 2026-07-18 | minor | Kickoff replaces seed-run stages F/G/H with a custom pipeline (Codex GPT-5.6 Sol high adversarial → Kimi K3 docs pass), all supervisor-dispatched. This session stops at `STAGE-COMPLETE: generator`; no self-arranged evals. | Recorded in `supervisor.md`; conforms to lane-policy invariants (generator ≠ evaluator). | +| 3 | 2026-07-18 | minor | Stage-B discovery ran on Claude Opus 4.8 Explore sub-agents (4-way fan-out) instead of Tier-C committed workflows. Rationale: `subagent-model-routing` standing rule (Fable prohibited for swarm use; Opus is the swarm lane) and CLAUDE.md's "workflows are an expensive supervisor accelerator" caveat. Findings are integrated and re-cited in `research.md`; load-bearing claims were spot-verified by the supervisor session (Fresh 2.3.3 `App.mountApp`/`Builder.registerIsland` verified directly via `deno doc`/jsr source). | Recorded here; citations in `research.md` are the audit trail. | diff --git a/.llm/runs/plan-frontend-contrib--seed/kickoff.md b/.llm/runs/plan-frontend-contrib--seed/kickoff.md new file mode 100644 index 000000000..bc81915a6 --- /dev/null +++ b/.llm/runs/plan-frontend-contrib--seed/kickoff.md @@ -0,0 +1,60 @@ +# Kickoff — plan-frontend-contrib--seed (Fable 5 · high) + +use harness. You are a **planning/seed agent** (Fable 5, effort high) designing the missing +**frontend contribution layer** for NetScript plugins. Drafts and design documents ONLY. + +## Owner mandate (verbatim intent) + +The frontend contribution layer of plugins is currently missing, and multiple future features are +limited without it: the **dev dashboard**, **auth**, **ai**, and now **deploy** (the new +deploy-plugin direction on branch `plan/deploy-plugin` will contribute cloud-specific frontend +seams). The owner wants an **ABSOLUTE PERFECT frontend contribution layer** that solves all these +limitations while being extremely elegant to work with, flexible, and **DX PERFECT**. The doc/API +story must force implementation to think DX first, not just implement. + +This caveat was identified long ago during dev-dashboard planning: grep the harness run history +(`.llm/runs/`, git log across `plan/*` branches, dashboard runs from beta-10) for the prior +analysis — e.g. `rtk grep -ri "frontend contribution\|plugin island\|plugin ui" .llm/runs/` and the +beta-10 dashboard run dirs — and treat what you find as evidence. + +## Mandate + +Produce a clean seed run in `.llm/runs/plan-frontend-contrib--seed/` on branch +`plan/frontend-contrib` (this worktree). Cover, at minimum: + +1. **Contribution model**: how a plugin contributes frontend surface (islands, routes, components, + nav entries, theme tokens) to a Fresh/fresh-ui host app — registration, discovery, type safety, + SSR/hydration boundaries, isolation. Contract first: schema/type contracts before mechanism. +2. **Consumers**: concrete contribution stories for the four blocked features — dev dashboard + panels, auth UI (sign-in flows, org/member management), ai surfaces, deploy plugin dashboards — + each as a worked example against the proposed API. +3. **Scaffolding + registry**: how `netscript` CLI scaffolds and regenerates frontend contribution + registries (precedent: existing plugin registry generation), and how contributions appear in a + scaffolded project. +4. **Doctrine fit**: archetype, public surface, gates, debt; precedence from shipped packages + (`@netscript/fresh-ui` namespaces, plugin registry patterns). Wrap Web Platform / Fresh + primitives; do not reinvent. +5. **DX**: the developer experience of authoring a contribution must be front and center — show + the exact code a plugin author writes, keep it minimal and harmonized across adapters/ports. + +Use `deno doc` on `@netscript/fresh-ui` and the plugin packages before broad reads. + +## Pipeline (supervisor-dispatched — do NOT self-dispatch downstream stages) + +1. YOU produce the seed run (plan.md, design/canonical/*, worked examples, worklog, drift). +2. When complete, write `STAGE-COMPLETE: generator` at the end of `worklog.md` and STOP. The + supervisor then dispatches a **Codex GPT-5.6 Sol high adversarial/collaborative** pass (enhance, + not ruin), you integrate fixes/enhancements, then a **Kimi K3** pass writes the public-facing API + + documentation story to make the final product concrete. +3. You will be resumed to integrate findings. + +## Stop-lines (verbatim, in force) + +- Drafts only: no GitHub issues, PRs, labels, or milestones may be created or changed by this run. +- No product code changes: no `packages/` or `plugins/` source edits. +- No merges without CI green + eval PASS + standing authorization; HARD STOP before any release + publish (release:cut, JSR publish, tag push, canary or stable) — owner sign-off in-turn only; + HARD STOP before closing milestone 13 — owner only. +- Do not self-arrange evals; the supervisor triggers every downstream stage. +- Commit to this branch only, push with explicit refspec: + `git push origin HEAD:plan/frontend-contrib`. diff --git a/.llm/runs/plan-frontend-contrib--seed/plan.md b/.llm/runs/plan-frontend-contrib--seed/plan.md new file mode 100644 index 000000000..3b9b23d7e --- /dev/null +++ b/.llm/runs/plan-frontend-contrib--seed/plan.md @@ -0,0 +1,92 @@ +# Plan — plan-frontend-contrib--seed (generator draft) + +> **Status: generator draft.** Locked = this session's recommendation with rationale; nothing is +> owner-ratified. Pending: Codex GPT-5.6 Sol adversarial pass → integration → Kimi K3 docs/API +> pass → owner review. Drafts only; no board mutations; no product code. + +## Scope + +Design (not implement) the frontend contribution layer: contracts, discovery/registry, host +runtime, scaffolding/CLI, DX, and four worked consumers. Deliverables are +`design/canonical/00–06` + `design/examples/{dashboard,auth,ai,deploy}` + this plan. + +## Locked design decisions (D#) — rationale in the canonical docs + +| # | Decision | Where | +| --- | --- | --- | +| D1 | New Archetype-1 contract package `@netscript/plugin-frontend-core` owns the vocabulary; depends on neither fresh nor fresh-ui; component refs are module specifiers (data) | 01 | +| D2 | Core `@netscript/plugin` gains only a pointer axis (`FrontendContributionRef` + `.withFrontend()` + manifest block) — resolves OQ-12: first-class axis, zero core vocabulary | 01, 06 | +| D3 | Base family v1 = route / island / zone / nav / theme; dashboard-only kinds (panel/action/ai-tool/entity-tab/home-card) are a family EXTENSION owned by the future `plugin-dashboard-core` | 01, examples/dashboard | +| D4 | Discovery = manifest pointer + generated registry (`.netscript/generated/frontend.*`) via the existing axis-emitter pipeline; runtime registration rejected (carried verdict) | 03 | +| D5 | Islands register by module specifier on both build paths (`islandSpecifiers` / `registerIsland`) — plugin authors import islands directly, zero wrapper API | 02, 04 | +| D6 | Routes mount as lazy sub-Apps via upstream `App.mountApp`; plugin-preferred `base` + host remap + generate-time collision errors | 04 | +| D7 | Two delivery models, both first-class: live (package-served, default) vs scaffolded starter (app-owned, via `AppTarget` on `ScaffolderContext`); rule: user-edits-it → scaffold, plugin-owns-it → serve | 00, 05 | +| D8 | Data path: server = typed client + `ctx.host.serviceUrl()`; client = same client via `/api/plugins//*` proxy middleware; `requires` declares the audit surface | 04 | +| D9 | `PluginHostState` is the only host-state seam plugin pages may assume (portability across app/dashboard hosts) | 04 | +| D10 | Theming: `--ns-*` semantic vars only, CSS layered (`@layer ns-plugins`) + `[data-ns-plugin]` scoping; DTCG token merge deferred | 04 | +| D11 | App surface trust = T0 (installed plugins already run server code); containment = lazy mounts, zone error boundaries, quarantine states; T1/T2 stay dashboard-scope | 04 | +| D12 | Contract versioning: `contracts/v1` subpath + `contract: 'v1'` handshake + install-time `deno check` gate + doctor + quarantine-not-crash (carried verbatim from ratified dashboard design §4) | 01, 03 | +| D13 | Scaffold templates wire the layer by default (registry import, islandSpecifiers feed, PluginZone placements, nav feed, css import) so `plugin install` produces visible surface with zero app edits | 05 | +| D14 | Explicit manifest is the contract; file-convention generator (`netscript generate frontend`) is phase-2 sugar that also maintains JSR explicit exports | 02 | + +## Owner forks (F#) — decisions reserved for ratification, none silently taken + +| # | Fork | Options | Seed recommendation | +| --- | --- | --- | --- | +| F1 | Core pointer axis | (a) `.withFrontend()` pointer in `@netscript/plugin` (b) manifest-only, core untouched | **(a)** — the axis is doctrine-anticipated; pointer keeps thinness | +| F2 | Route mount policy | (a) plugin base + host remap (b) forced `/plugins/` namespace on app surface | **(a)** — `/auth/org` beats `/plugins/auth/org` for end users; collisions caught at generate time | +| F3 | Contract package name | (a) `@netscript/plugin-frontend-core` (b) fold into `@netscript/fresh` (c) `@netscript/frontend-core` | **(a)** — sibling-symmetric with `plugin-*-core`; keeps fresh-free contracts | +| F4 | v1 zone set | minimal 4 (`topbar.end`, `dashboard.panels`, `home.cards`, `footer`) vs larger | **minimal 4** — zones are additive-minor later | +| F5 | Theme contributions | (a) CSS overlay only (b) + DTCG token-file merge into `tokens:build` | **(a)** at v1 | +| F6 | Convention generator timing | phase 1 vs phase 2 | **phase 2** — explicit form is the contract; sugar follows proof | +| F7 | First-party dogfood set for phase 1 | one plugin (ai chat) vs four capability panels | **ai + workers** (one live route + one zone panel) — proves both surfaces cheaply | +| F8 | Milestone placement | beta.12 … beta.14 vs stable train | defer — depends on PM #510 (beta.12) and dashboard epic sequencing; note deploy epic (#823 family) wants this layer early | + +## Phasing (implementation waves, post-ratification — each wave is normal run-loop work) + +1. **Wave 1 — contracts + spine**: `plugin-frontend-core` (contracts/v1, defineFrontend, + definePluginPage), pointer axis in `@netscript/plugin`, manifest block, registry emissions, + `@netscript/fresh/plugins` glue (mount, proxy, nav, PluginZone, withPluginIslands), + `defineFreshApp.frontend`, vite feed, scaffold template wiring, dogfood per F7. + Gate: extended `scaffold.runtime` assertions (05 §5) + SCOPE-frontend browser gates. +2. **Wave 2 — DX + lifecycle**: `plugin new --with frontend`, `generate frontend` convention + generator + exports maintenance, `generate frontend-wiring` adoption verb, doctor `frontend` + check, quarantine render states, `plugin resource add --app` (`AppTarget` seam). +3. **Wave 3 — consumers**: auth frontend (starter signin + live org console + session widget), + ai frontend (live chat + assist launcher; starter repositioned), remaining first-party panels; + deploy + dashboard epics consume the layer in their own runs. + +## Gates & validation map (per `gates/archetype-gate-matrix.md` + SCOPE-frontend) + +- `plugin-frontend-core`: Arch-1 column (F-1/5/6/7/8/10/11/12/14–19), `deno doc --lint`, + jsr-audit at publish wave. +- `packages/fresh` `./plugins`: Arch-3 column + SCOPE-frontend (route renders, browser + validation, loading/empty/error states, responsive) + consumer-import validation. +- `packages/cli`: Arch-6 + relevant F-CLI-*; scaffold-static; extended `scaffold.runtime` E2E + (install→render→hydrate→uninstall) at merge-readiness only. +- Plugins: Arch-5 + `quality:scan` + `arch:check` per the #745 lesson (wrappers alone are not a + verdict). + +## Risk register + +| Risk | Exposure | Mitigation in design | +| --- | --- | --- | +| Vite island registration from jsr/workspace specifiers has unproven edge cases (dedupe, HMR) | Wave 1 spine | primitive verified upstream (`mod.ts:211-214`); Wave 1 starts with a spike slice; non-vite builder path is the fallback proof | +| Dep-CSS `@import` through vite | theming | copy-into-generated fallback predeclared (03 §2) | +| JSR explicit-exports friction for island/route modules | authoring | generator-maintained exports (D14); documented interim rule | +| SSE/streaming through the plugin proxy | ai example | existing stream-proxy precedent; keep proxy thin (`app.use` passthrough) | +| Route/base collisions with user apps | UX | generate-time structured errors + host remap (D6) | +| Scope creep into dashboard family | this seed | D3 boundary + out-of-scope table (00) | +| Fresh 2.x API drift (pre-3.0 churn) | spine | wrap-points concentrated in `@netscript/fresh/plugins`; upstream pin `^2.3.3` | + +## Debt candidates (file on implementation, not now) + +See `design/canonical/06-doctrine-fit.md §4` (token-only CSS lint, tailwind content scan, DTCG +merge, island-props check, zone caps). + +## Evidence base + +`research.md` (all claims cited); prior-art: +`dashboard-design--orchestrator/analysis/plugin-extension-architecture.md` (generalized by D3), +roadmap-expansion A-dashboard §9 / OQ-12 (resolved by D2), deploy-plugin kickoff (owner intent for +seam 2), doctrine 07 (axis table row made real). diff --git a/.llm/runs/plan-frontend-contrib--seed/research.md b/.llm/runs/plan-frontend-contrib--seed/research.md new file mode 100644 index 000000000..c950a0776 --- /dev/null +++ b/.llm/runs/plan-frontend-contrib--seed/research.md @@ -0,0 +1,193 @@ +# Research — plan-frontend-contrib--seed + +Stage-B corpus for the frontend contribution layer. Discovery ran as a 4-way Opus Explore fan-out +(prior-analysis mining · plugin/registry mechanism · fresh/fresh-ui surface · consumer state) plus +supervisor-session verification of the load-bearing upstream claims. Every claim below carries a +citation; sub-agent findings that were load-bearing were re-verified directly (marked ✔). + +## 1. The limitation, as previously analyzed + +The caveat — plugins cannot contribute frontend surface — has a three-run analysis lineage: + +1. **plan-roadmap-expansion--seed (A-dashboard, ~2026-07-05)** — first written architecture: + `design/A-dashboard/proposal.md:521-549` (§9.2) defines a `DashboardPanelContribution` contract + in a `plugin-dashboard-core/contracts/v1`, discovered "the way Aspire contributions are + discovered", explicitly keeping `@netscript/plugin` dashboard-agnostic. Filed as DDX-17 + (`epic-and-issues.md:298-313`, later issue #427). Open question OQ-12 + (`open-questions.md:94-101`): contribution-contract seam vs a first-class core `definePlugin` + axis — never resolved. +2. **dashboard-rescope--seed (2026-07-06)** — `plan.md:105` makes the seam a cross-cutting law + ("the dashboard is itself a plugin (dogfood), and third-party plugins contribute panels through + the same typed axis"); #427 verdict KEEP-AS-IS (tighten) (`issues-rescope.md:548-556`). +3. **dashboard-design--orchestrator (2026-07-12)** — the definitive 437-line proposal + `analysis/plugin-extension-architecture.md`: a 7-member `DashboardContribution` discriminated + union (panel/route/action/ai-tool/nav/entity-tab/home-card), injection zones (Medusa-derived), + generated-registry discovery (runtime `registerPanel()` explicitly rejected), trust tiers + T0/T1/T2, `contributesTo: { dashboard: 'v1' }` version handshake, and — §5 — the "second half": + contribution into the USER's app via the existing `PluginAdapter.toScaffold()` engine plus a + missing `AppTarget` seam on `ScaffolderContext`. Appendix A is a cited teardown of TanStack + Devtools, Nuxt DevTools, Directus, and Medusa extension models. + +**Nothing shipped.** `packages/plugin-dashboard-core` does not exist; `DashboardPanelContribution` +appears in zero product source files; `find plugins -name "*.tsx"` is empty. Beta-10 shipped a +Claude Design prototype rendering the system as-if-shipped +(`dashboard-design--orchestrator/prototype/`), not code. No arch-debt entry names the gap directly; +adjacent: `arch-debt.md:1001-1011` (generated frontend route React-reference failure), +`arch-debt.md:2120-2134` (Archetype-5 contribution-folder placement). + +Doctrine already anticipates the axis: `docs/architecture/doctrine/07-composition-and-extension.md:101` +lists "Frontend framework | fresh (today), future expansion" in the extension-axis table; the same +file mandates registration-over-inheritance (`:114-134`), deterministic plugin load order with +duplicate rejection (`:135-153`), and the `extension-points.ts` manifest rule R-COMP-EXT-MANIFEST +(`:255-289`). + +## 2. The plugin system today (the precedent to extend) + +- **Authoring DSL**: `definePlugin` fluent builder — + `packages/plugin/src/config/builders/plugin-builder.ts:60-310`. Contribution axes + (`packages/plugin/src/config/domain/plugin-contributions.ts:12-38`): services, + backgroundProcessors, streamTopics, databaseSchemas, runtimeConfigTopics, contractVersions, e2e, + telemetry, migrations, aspire, cli.doctorChecks. **No frontend axis.** `PluginType` includes an + inert `'frontend'` enum value (`packages/plugin/src/domain/constants.ts:5-13`) with no machinery. +- **Installer manifest**: `scaffold.plugin.json`, schema + `packages/plugin/src/protocol/manifest.ts:112-136` (+ Zod `:204-216`, `schemaVersion 1`). + `capabilities.hasRoutes` is a bare boolean; `scaffolder.export` points at a published scaffold + entrypoint executed in a permission-scoped subprocess. Parsed without executing plugin code + (`parsePluginManifest`, `:240-263`). +- **`*-core` split**: `packages/plugin--core` = pure publishable contracts/domain (e.g. + `packages/plugin-auth-core/deno.json:1-18` exports `./contracts/v1` etc.); `plugins/` = the + installable runnable plugin owning manifest + services + scaffolder. +- **Registry generation**: `netscript generate plugins` + (`packages/cli/src/public/features/generate/plugins/generate-plugin-registries-command.ts:41-84`) + runs Walker→Extractor→Emitter SDK ports (`packages/plugin/src/sdk/mod.ts:11-23`) and emits one + generated module per axis at `.netscript/generated/.registry.ts` + (`packages/plugin/src/sdk/discovery/registry-emitter.ts:14-17`), marked + "AUTO-GENERATED / DO NOT EDIT" (`:52-54`), idempotent by construction (`:11-19`). +- **Install flow**: `installPlugin` + (`packages/cli/src/public/features/plugins/install/install-plugin.ts:98-202`) — local-path or JSR + descriptor, dispatches the plugin-owned scaffolder, then host wiring: appsettings, config, + imports, workspace member, Aspire helper regeneration (`:145-192`). Plugin files land under + `plugins/` in the project (`:392-397`). +- **Codegen idempotency precedent**: `generate runtime-schemas` skips byte-identical writes and + enforces single-owner-per-topic + (`packages/cli/src/public/features/generate/runtime-schemas/generate-runtime-schemas.ts:107-176`). + +## 3. The frontend framework surface (what a contribution layer builds on) + +### 3.1 Upstream Fresh 2.3.3 primitives (✔ verified directly) + +- `App` composable router: `use()`, `route()`, `get/post/…`, `layout()`, `appWrapper()`, + `fsRoutes(pattern)`, and **`mountApp(path, app)`** — sub-app composition + (✔ `deno doc jsr:@fresh/core@2.3.3`, class App; `https://jsr.io/@fresh/core/2.3.3/src/app.ts:357`). +- Islands are build-time registered. Non-vite: **`Builder.registerIsland(specifier)`** + (✔ `https://jsr.io/@fresh/core/2.3.3/src/dev/builder.ts:157`; islands dir crawl feeds the same + call, `:286-295`). Vite: **`fresh({ islandSpecifiers: string[] })`** on `@fresh/plugin-vite` + (✔ `https://jsr.io/@fresh/plugin-vite/1.0.8/src/mod.ts:56-63,211-214`) — arbitrary module + specifiers (jsr/npm/workspace deps included) become islands. The scaffold pins + `@fresh/plugin-vite@^1.0.8` (`packages/cli/src/kernel/adapters/scaffold/fresh-adapter.ts:145`). + **Correction to sub-agent report**: the fresh-ui survey concluded "no existing seam" for + dependency islands — true of NetScript's current wiring, false upstream; the seam exists on both + build paths and is merely unused. +- `Island` runtime shape `{ file, name, exportName, fn, css[] }` + (`https://jsr.io/@fresh/core/2.3.3/src/context.ts:88-96`). + +### 3.2 `@netscript/fresh` (the wrap layer) + +- `defineFreshApp` (`packages/fresh/src/runtime/server/define-fresh-app.ts:110-127`) wraps `App`, + `staticFiles`, `app.use`, `app.fsRoutes`, with seams `middleware[]`, `preConfigure`, `configure`, + `createApp` — middleware/route injection points exist; no island or contribution seam. +- Typed route manifest generator: `packages/fresh/src/application/route/manifest.ts` writes + app-local `.generated/manifest.ts` + `.generated/routes.ts`; apps re-export via `router.ts` with + `createRouteReference` (`packages/cli/src/kernel/assets/app/router.ts.template:1-49`) — the + typed-route registry precedent. +- Vite integration: `createNetScriptVitePlugin` (`packages/fresh/src/application/vite/README.md`) — + `@app/*` aliases, Preact dedupe, route-manifest generation. Today the scaffold's + `vite.config.ts.template` calls bare `fresh()` (`packages/cli/src/kernel/assets/app/vite.config.ts.template:1,44`). +- `@netscript/fresh/ai` (`packages/fresh/src/runtime/ai/mod.ts:1-101`): durable-chat server+island + seam (`createNetScriptChatConnection`, `projectChatSnapshot`, stream proxy) — the AI runtime + plane already exists; only its mounting story is missing. + +### 3.3 `@netscript/fresh-ui` (the design system) + +- Dual nature: small runtime surface (root `mod.ts` helpers: Icon, DataGrid, toast, `cn` — + `packages/fresh-ui/mod.ts:12-40`; interactive `*Namespace` compounds — + `packages/fresh-ui/interactive.ts:22-113`) + a **copy-registry**: ~60 component/css pairs + + islands copied into the app, app-owned afterwards (`packages/fresh-ui/registry.ts:12-60` — + `FreshUiRegistryItem { name, kind: component|style|theme|lib, files[{source,target}], css… }`). +- CLI engine: `netscript ui add|init|list|update|remove` + (`packages/cli/src/kernel/application/ui/registry.ts:81-196`) — copies with import rewriting to + `@ui/ @islands/ @assets/ @lib/` targets, merges deno.json imports + a CSS aggregator, detects + local-edit drift on update. **The closest existing thing to a frontend contribution model, and + the reason islands work today: registry islands are copied into the app's `islands/` dir.** +- Theme system: CSS custom properties `--ns-*`; DTCG sources `tokens/semantic.tokens.json` (each + token carries `$extensions.netscript.cssVar`), themes under `tokens/themes/`, generated + `registry/theme/tokens.css` + Tailwind v4 bridge `theme-bridge.css` (`deno task tokens:build`); + delivered via the `theme-seed` registry item (`packages/fresh-ui/registry.manifest.ts:141-176`); + runtime switching via `data-theme` attribute. +- Nav/shell: `SidebarShell` + `SidebarNavItem`/`SidebarNavSection` + (`packages/fresh-ui/registry/components/ui/sidebar-shell.tsx:8-36`) — nav is a static + `readonly SidebarNavSection[]` prop; scaffolded layouts hardcode arrays + (`DESIGN_NAVIGATION` in the design layout template; topbar entries in + `packages/cli/src/kernel/assets/app/routes/_layout.tsx.template:33-74` reading the typed + `appRoutes`). + +## 4. Consumer state (the four blocked features) + +- **Auth**: mature backend — oRPC contract `signin/callback/signout/session/me` + (`packages/plugin-auth-core/src/contracts/v1/auth.contract.ts:437-459`), three adapters + (better-auth with `organization/twoFactor/admin/apiKey` plugins — + `packages/auth-better-auth/src/better-auth.ts:56,132-138`; workos with `organizationId` — + `packages/auth-workos/src/workos-backend.ts:62,173`; kv-oauth full HTTP flow — + `packages/auth-kv-oauth/src/flow.ts:69-233`). **Zero `.tsx`** anywhere in auth packages. + Needs: sign-in page, session/me widget, sign-out, org/member management UI. +- **AI**: oRPC `chat` (SSE eventIterator) + `models` + (`packages/plugin-ai-core/src/contracts/v1/ai.contract.ts:317-391`); **the one existing + plugin→app frontend precedent**: `chatRouteScaffolder` emits `ai/routes/chat.tsx` into the app + (`plugins/ai/src/adapter/resources/chat-route/chat-route.ts:33`), whose stub renders a + `ChatIsland` consuming `@netscript/fresh/ai` (`chat-route.stub.ts:19-24`). fresh-ui already + ships chat components (message, prompt-input, model-selector, tool-call-card, + `src/chat/`, `src/ai/render-ui.tsx`). Owner direction (dashboard brief Axis 5): AI is + *distributed* — contextual actions, procedures-as-tools — not one chat pane. +- **Dev dashboard**: no dashboard package/plugin exists; scaffold ships a static `/dashboard` + route + `/design` gallery with hardcoded panel JSX and nav arrays + (`packages/cli/src/kernel/application/scaffold/writers/app-route-seeds.ts:6-92`; + `dashboard.tsx.template`). The 7-member dashboard contribution family is designed (see §1.3) + but unbuilt. +- **Deploy**: sibling seed run `plan-deploy-plugin--seed` is at stage-A bootstrap (branch + `plan/deploy-plugin`, HEAD `d7879e68`); owner-ratified intent (its `kickoff.md`, quoting the + #824 redirect): "the plugin allow us to contribute to every layers (**even frontend soon**) + meaning we could imagine SCAFFOLDING a cloudflare optimized project that would ships seams that + already are cloudflare first (e.g. workers, durable object, kV, ...) same story for AWS". + Adapter op set `plan/emit/up/down/status/logs` + (`.llm/harness/archetypes/ARCHETYPE-7-deploy-target-adapter.md`) is what deploy panels/actions + would surface. Needs both seams: cloud-optimized scaffolding into the app AND live + status/logs/actions consoles. + +## 5. External models (carried from prior teardown, re-cited) + +`dashboard-design--orchestrator/analysis/plugin-extension-architecture.md` Appendix A (verified +2026-07-12 against vendor docs): TanStack Devtools (runtime plugin array + namespaced event bus), +Nuxt DevTools (iframe tabs + birpc), Directus (manifest permissions + V8-isolate sandbox + +marketplace compat gating), Medusa (build-time widgets + published injection-zone enum + file-based +admin routes). Synthesis adopted there and reused here: manifest + generated type-checked registry +(the "hard end" of the registration spectrum, matching how `generate plugins` already works); +injection zones as versioned contract surface; explicit contract-version handshake. + +## 6. Synthesis — the design space this run must resolve + +1. **Two delivery models, both with repo precedent**: *live/package-served* UI (generated registry + imports plugin exports; islands via `islandSpecifiers`/`registerIsland`; versioned, upgradeable) + vs *copy/scaffolded* UI (fresh-ui registry + chat-route precedent; app-owned, customizable, + drift-managed). A correct design uses both deliberately, per contribution kind. +2. **Where the vocabulary lives** (OQ-12): thinness law says not in `@netscript/plugin`; the + doctrine axis table says frontend is a real extension axis. Resolution space: manifest-pointer + axis in core + typed family in a new `*-core` contract package. +3. **Generalization**: the dashboard family (7 kinds) is one *host*'s vocabulary. The user app is + another host. The layer must define the host-agnostic base (routes, islands, components/zones, + nav, theme) such that the dashboard family becomes an extension of it, not a sibling. +4. **Missing seams to specify** (all additive): manifest `frontend` block; `frontend` registry + emission; vite `islandSpecifiers` feed; `defineFreshApp` mount glue; nav feed into + `SidebarShell`/topbar; typed route references for plugin routes; CSS/theme aggregation; + `AppTarget` on `ScaffolderContext`; CLI verbs. +5. **Trust**: user-app contributions are installed code (already runs server-side) — T0 by + definition; dashboard T1/T2 tiers remain the dashboard run's scope. diff --git a/.llm/runs/plan-frontend-contrib--seed/supervisor.md b/.llm/runs/plan-frontend-contrib--seed/supervisor.md new file mode 100644 index 000000000..ee2cc26be --- /dev/null +++ b/.llm/runs/plan-frontend-contrib--seed/supervisor.md @@ -0,0 +1,34 @@ +# Supervisor Identity — plan-frontend-contrib--seed + +Written at run start per `workflow/lane-policy.md` § Supervisor identity. + +| Field | Value | +| --- | --- | +| Model | Claude Fable 5 (`claude-fable-5`), effort high | +| Session | background job `50b02c18` — https://claude.ai/code/session_015fc7SaneZJ9mzrAhRW4tma | +| Host | WSL2 (Linux 6.18.33.2-microsoft-standard-WSL2), user `codex` | +| Checkout | `/home/codex/repos/netscript` (main checkout) | +| Worktree | `/home/codex/repos/wt-frontend-contrib` | +| Branch | `plan/frontend-contrib` | +| Baseline | `290c68ef` (main, 2026-07-18) | +| Run ID | `plan-frontend-contrib--seed` | + +## Routes in force + +| Task lane | Provider / model / effort | Role in this run | +| --- | --- | --- | +| Seed generator (this session) | Anthropic / Claude Fable 5 / high | Discovery, synthesis, design docs, worked examples — drafts only | +| Research fan-out sub-agents | Anthropic / Claude Opus 4.8 (per `subagent-model-routing` memory: Fable prohibited for swarm use) | Read-only repo exploration feeding `research.md` | +| Adversarial pass (stage 2) | OpenAI / GPT-5.6 Sol / high — **supervisor-dispatched, not by this session** | Enhance/critique the seed design | +| Docs/API concretion (stage 3) | Moonshot / Kimi K3 — **supervisor-dispatched, not by this session** | Public-facing API + documentation story | + +## Recorded lane/eval overrides + +- **No draft PR for this run** (deviation from `seed-run.md` stage A): kickoff stop-line forbids + creating PRs/issues/labels/milestones. Commit trail is direct commits on `plan/frontend-contrib` + pushed with explicit refspec. Mirrored in `drift.md`. +- **Custom pipeline** (deviation from seed-run stages F/G/H): kickoff prescribes + generator → Codex Sol adversarial → Kimi K3 docs pass, all dispatched by the human-side + supervisor. This session does not self-arrange evals and stops at `STAGE-COMPLETE: generator`. +- **No board filing stage**: deliverable is design drafts, not GitHub issues. Stage H/I do not + apply to this leg. diff --git a/.llm/runs/plan-frontend-contrib--seed/worklog.md b/.llm/runs/plan-frontend-contrib--seed/worklog.md new file mode 100644 index 000000000..c55610c3e --- /dev/null +++ b/.llm/runs/plan-frontend-contrib--seed/worklog.md @@ -0,0 +1,57 @@ +# Worklog — plan-frontend-contrib--seed + +## 2026-07-18 — Bootstrap + +- Harness activated (`use harness`); kickoff read verbatim; seed-run profile with recorded + overrides (no draft PR per stop-lines; custom Sol/Kimi pipeline — see `supervisor.md`, + `drift.md` #1–#2). +- `supervisor.md` written at run start. Baseline `290c68ef` (main, 2026-07-18). + +## 2026-07-18 — Discovery (stage B) + +- 4-way Opus 4.8 Explore fan-out (drift #3): prior-analysis mining, plugin/registry mechanism, + fresh/fresh-ui surface, consumer state. Integrated + re-cited in `research.md`. +- Supervisor verification of load-bearing upstream claims via `deno doc` + jsr source: + `App.mountApp` (@fresh/core 2.3.3 app.ts:357), `Builder.registerIsland` (dev/builder.ts:157), + `fresh({ islandSpecifiers })` (@fresh/plugin-vite 1.0.8 mod.ts:56-63, 211-214) — including a + **correction** of the fan-out finding "no dependency-island seam exists" (true of NetScript + wiring, false upstream). +- Key precedent set identified: axis registry emitter, `scaffolder.export` pointer idiom, + fresh-ui copy-registry + `--ns-*` tokens, ai chat-route scaffolder, typed route refs, + ratified dashboard extension architecture (437-line prior design). + +## 2026-07-18 — Design (checkpoint) + +### Design + +- **Contract-first**: family (route/island/zone/nav/theme) + manifest + pointer in + `design/canonical/01-contracts.md`; new Archetype-1 package `@netscript/plugin-frontend-core`. +- **DX-first**: authoring surface fixed as the bar (`02-authoring-dx.md`) — ordinary Fresh code + in `plugins//frontend/`, one `defineFrontend()` file, islands imported directly, typed + clients end-to-end, `--ns-*` tokens for automatic theming. +- **Mechanism**: discovery/registry (`03`), host runtime (`04`), scaffolding/CLI + AppTarget + starter model (`05`), doctrine fit (`06`). +- **Consumers**: four worked examples with exact author code + (`design/examples/{dashboard,auth,ai,deploy}.md`), each mapping its blocked need onto the API + and choosing live vs starter delivery deliberately. +- **Plan**: D1–D14 locked (generator level), F1–F8 owner forks reserved, 3 implementation waves, + gate map, risk register (`plan.md`). + +Design self-check against the kickoff mandate: contribution model ✓ (contracts before mechanism), +consumers ✓ (4 worked examples), scaffolding+registry ✓ (existing pipeline extended, idempotent), +doctrine fit ✓ (archetype table, layering laws, debt candidates, precedence table), DX ✓ (doc 02 +is the normative bar; `deno doc` used for surface research throughout). + +## Gate log + +Planning-only run: no code gates run. Artifact hygiene: all design docs carry draft/no-mutation +banners; citations verified during writing. Stop-lines honored: no GitHub mutations, no +`packages/`/`plugins/` edits, commits confined to `.llm/runs/plan-frontend-contrib--seed/`. + +## Handoff + +Per kickoff pipeline: supervisor dispatches the Codex GPT-5.6 Sol high adversarial pass next; +this session (resumed with `context-pack.md`) integrates findings; then the Kimi K3 docs/API +pass. This session self-arranged no evals. + +STAGE-COMPLETE: generator From d8acb1e6c5440362dc0845a9fa38e1f2c55aef03 Mon Sep 17 00:00:00 2001 From: Rickylabs Date: Sun, 19 Jul 2026 05:54:23 +0200 Subject: [PATCH 02/11] plan(frontend-contrib): stage-2 adversarial brief (Sol high) Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_015fc7SaneZJ9mzrAhRW4tma --- .../briefs/adversarial-sol-brief.md | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 .llm/runs/plan-frontend-contrib--seed/briefs/adversarial-sol-brief.md diff --git a/.llm/runs/plan-frontend-contrib--seed/briefs/adversarial-sol-brief.md b/.llm/runs/plan-frontend-contrib--seed/briefs/adversarial-sol-brief.md new file mode 100644 index 000000000..fc42ae0e2 --- /dev/null +++ b/.llm/runs/plan-frontend-contrib--seed/briefs/adversarial-sol-brief.md @@ -0,0 +1,85 @@ +use harness + +# Adversarial/Collaborative Review — frontend contribution layer seed design + +You are the **stage-2 adversarial reviewer** (Codex GPT-5.6 Sol, effort high) for the seed run +`.llm/runs/plan-frontend-contrib--seed/` on branch `plan/frontend-contrib`. The generator (a +Claude Fable 5 session) produced a design for the missing **plugin frontend contribution layer**. +Your mandate from the owner: **enhance, not ruin** — attack the design hard, but every finding +should either kill a real flaw or make the design stronger. The generator will integrate your +legitimate findings afterwards; you do not edit the design docs yourself. + +## SKILL + +Read these skills/files before reviewing (in order): + +1. `.agents/skills/netscript-harness/SKILL.md` — run mechanics (you are the stage-F analog of a + seed run; findings only, supervisor integrates). +2. `.agents/skills/netscript-doctrine/SKILL.md` + `docs/architecture/doctrine/07-composition-and-extension.md` + — layering/extension laws the design claims to honor. +3. `.agents/skills/deno-fresh/SKILL.md` — Fresh 2.x mechanics (islands, routes, vite plugin). +4. The run artifacts, in this order: `research.md`, `plan.md`, `design/canonical/00-overview.md` + through `06-doctrine-fit.md`, `design/examples/{dashboard,auth,ai,deploy}.md`. + +## What to attack (minimum coverage) + +1. **Upstream API claims** — the design's load-bearing facts: `App.mountApp(path, app)` semantics + (basePath/middleware/layout interaction for mounted sub-apps), `fresh({ islandSpecifiers })` + behavior in `@fresh/plugin-vite@1.0.8` (do dependency-resolved specifiers actually build? + HMR? css handling? Preact dedupe interactions with the NetScript vite plugin?), lazy route + modules via `app.route(path, MaybeLazy)`. Verify against the actual jsr sources + (`https://jsr.io/@fresh/core/2.3.3/...`, `https://jsr.io/@fresh/plugin-vite/1.0.8/...`) or + local cache — cite what you verify. If a mechanism claim is wrong or riskier than stated, that + is a BLOCKER-class finding. +2. **Contract family** (`01-contracts.md`) — missing kinds/fields, schema-evolution traps, + the `ComponentRef`/specifier resolution model, the pointer-axis (D2) vs thinness, zone enum + design, `PluginHostState` adequacy (sessions, i18n?, CSP nonces?, base-path composition). +3. **Discovery/registry** (`03`) — generated-file set completeness, JSR explicit-exports + friction, local-source vs jsr resolution differences, uninstall cleanliness, collision rules, + quarantine mechanics, `deno check` gate realism. +4. **Host runtime** (`04`) — SSR/hydration gaps, the API proxy design (auth header forwarding, + SSE, security of `/api/plugins/*`), CSS layering/scoping realism, nav integration, error + containment claims (can a broken zone component really not break the page?), Tailwind + restriction implications. +5. **DX** (`02`) — is the authoring story actually minimal? Hidden footguns (island props + serialization, deno.json exports maintenance, dev-loop watch behavior for jsr-installed vs + local-source plugins)? Would YOU enjoy authoring against this API? Propose concrete + improvements. +6. **Worked examples** — do they hold against the real backend surfaces they cite + (auth contracts, ai SSE chat, deploy op set)? Is the live-vs-starter split right for each? +7. **Plan** — wave ordering, gate realism, risk register completeness, owner-fork framing + (any decision taken that should be a fork? any fork that has an obviously correct answer?). +8. **What's missing entirely** — i18n, a11y, CSP/nonce policy, mobile, versioned asset caching, + plugin frontend testing story, performance budgets, anything the four consumers will need + that the contracts cannot express. + +## Output contract (findings only — DO NOT edit design docs) + +Write exactly one file: `.llm/runs/plan-frontend-contrib--seed/adversarial-sol.md`, structured: + +``` +# Adversarial Review — Sol high (stage 2) +## Verdict summary <3-6 lines: overall soundness + top risks> +## Findings +### S-1 [severity: blocker|major|minor|enhancement] [area: contracts|discovery|runtime|dx|examples|plan|missing] +Claim under attack: <quote/cite file:line> +Evidence: <what you verified, with citations/urls> +Finding: <the flaw or enhancement> +Proposal: <concrete fix/enhancement the generator can apply> +### S-2 … +## Verified-claims log <upstream/repo claims you checked and CONFIRMED, one line each> +``` + +Number every finding. Severity honestly: a `blocker` means the design as written would fail +implementation; `enhancement` means the design works but you propose better. + +## Hard constraints + +- Do NOT modify any file except creating `adversarial-sol.md` (and nothing under `packages/`, + `plugins/`, `docs/`, `.github/`). +- No GitHub mutations (no PRs/issues/labels). No `deno cache --reload`. Do not delete lock files. +- Commit exactly once: message + `plan(frontend-contrib): adversarial Sol findings (stage 2)`, then push with the explicit + refspec `git push origin HEAD:refs/heads/plan/frontend-contrib`. Never bare `git push`. +- When done, end your final message with `DONE` on its own line. If blocked, end with + `BLOCKED: <reason>`. From 4d2647c896d1ca025ca0eb2fcb92d0d4a9068413 Mon Sep 17 00:00:00 2001 From: Rickylabs <eric.chautems@gmail.com> Date: Sun, 19 Jul 2026 06:17:16 +0200 Subject: [PATCH 03/11] plan(frontend-contrib): adversarial Sol findings (stage 2) --- .../adversarial-sol.md | 138 ++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 .llm/runs/plan-frontend-contrib--seed/adversarial-sol.md diff --git a/.llm/runs/plan-frontend-contrib--seed/adversarial-sol.md b/.llm/runs/plan-frontend-contrib--seed/adversarial-sol.md new file mode 100644 index 000000000..e2b4ec54b --- /dev/null +++ b/.llm/runs/plan-frontend-contrib--seed/adversarial-sol.md @@ -0,0 +1,138 @@ +# Adversarial Review — Sol high (stage 2) +## Verdict summary +The contribution-axis shape is promising, but the current design is not implementation-safe: three load-bearing Fresh/Vite assumptions are false or unproved, and the proposed proxy is not a defensible security boundary. +The contracts also conflate package identity, install identity, server-only request context, and browser-serializable state; the dashboard family extension does not fit the supposedly stable base schema. +Discovery needs literal, type-checked module edges and transactional regeneration, while the examples must be reconciled with the real auth, AI, and deploy surfaces. +A proof-first Wave 0 can retain the design's good axis/pointer direction while removing these blockers before public contracts are frozen. + +## Findings +### S-1 Mounted sub-app command ordering defeats both plugin middleware and host layouts [severity: blocker] [area: runtime] +Claim under attack: `design/canonical/04-host-runtime.md:28-50` registers plugin layouts/routes, then calls `sub.use(pluginScopeMiddleware(...))`, and says the host root layout still applies to the mounted sub-app. +Evidence: Fresh applies commands in insertion order. When it encounters a route it immediately snapshots the current segment middleware/layout chain ([`commands.ts` lines 222-300](https://jsr.io/@fresh/core/2.3.3/src/commands.ts#L222-L300)); middleware appended later is not retroactive. `App.mountApp()` prefixes the mounted commands' paths and base path, but passes the child `App` and `NotFound` commands through unscoped ([`app.ts` lines 353-385](https://jsr.io/@fresh/core/2.3.3/src/app.ts#L353-L385)). NetScript's host calls `configure()` before filesystem routes/layouts are registered (`packages/fresh/src/runtime/server/define-fresh-app.ts:83-97`), so plugin routes added by `configure()` precede the host `_app`/layout commands. +Finding: As sketched, `pluginScopeMiddleware` does not wrap the already-compiled plugin routes, and filesystem host layouts cannot wrap routes registered earlier. The two properties on which route scoping and shell integration depend therefore fail. A mounted child `App`/`NotFound` could also leak beyond the intended mount. +Proposal: Add a dedicated post-filesystem-route frontend-composition phase. In each child app, register middleware first, then layouts, then routes; reject or strip child `App`/`NotFound` contributions. Prove the exact order with integration tests covering host `_app`, nested host layouts, host `config.basePath`, plugin middleware, plugin 404s, and two mounted plugins before retaining D6. + +### S-2 An ordinary Fresh route module is not a valid `App.route()` lazy value [severity: blocker] [area: runtime] +Claim under attack: `design/canonical/01-contracts.md:101-114` and `04-host-runtime.md:30-38,141-148` say a generated specifier can be passed as `() => import(specifier)` to `app.route(path, MaybeLazy)` and that the module can be an ordinary Fresh route module. +Evidence: `App.route()` accepts `MaybeLazy<Route>` ([`app.ts` lines 256-262](https://jsr.io/@fresh/core/2.3.3/src/app.ts#L256-L262)); `Route` is an internal object with `component`, `handler`, `config`, and `css`, while `MaybeLazy<T>` resolves directly to `T` ([`types.ts` lines 55-72](https://jsr.io/@fresh/core/2.3.3/src/types.ts#L55-L72)). A filesystem route module instead exports `default`, `handler`/`handlers`, `config`, and `css`, and only the filesystem router normalizes that namespace into a `Route` ([`fs_routes.ts` lines 20-28 and 143-176](https://jsr.io/@fresh/core/2.3.3/src/fs_routes.ts#L143-L176)). The proposed computed `import(m.specifierOf(...))` also hides the module graph from Vite/Rollup. +Finding: The proposed lazy loader returns the wrong object shape, omits the normalization step, and cannot provide a statically discoverable build edge. Route CSS, handlers, and layouts are consequently not reliably registered. +Proposal: Generate literal loaders, one per contribution, such as `() => import("@scope/plugin/frontend/routes/foo").then(normalizeFreshRouteModule)`, where a Fresh-owned adapter explicitly maps `default`, `handler`/`handlers`, `config`, and `css` to the pinned `Route` shape. Add a separate layout contribution/normalizer or explicitly forbid plugin route layouts in v1. Type-check the generated loader module, not just the manifest. + +### S-3 Dependency-resolved island builds are plausible, not yet a verified primitive [severity: blocker] [area: discovery] +Claim under attack: `plan.md:74` and `design/canonical/04-host-runtime.md:52-63` treat `fresh({ islandSpecifiers })` as a verified path for JSR-installed plugin islands, with development and CSS behavior implied by `02-authoring-dx.md:125-141`. +Evidence: Plugin Vite 1.0.8 does turn each configured island specifier into a Rollup input/dynamic import ([`client_snapshot.ts` lines 72-84 and 119-140](https://jsr.io/@fresh/plugin-vite/1.0.8/src/plugins/client_snapshot.ts#L119-L140)). However, its Deno resolve/load plugins are constructed with `cachedOnly: true` ([`deno.ts` lines 40-51](https://jsr.io/@fresh/plugin-vite/1.0.8/src/plugins/deno.ts#L40-L51)); its automatic island watcher only observes the configured local islands directory and route `_islands` directories ([`client_snapshot.ts` lines 86-103 and 161-173](https://jsr.io/@fresh/plugin-vite/1.0.8/src/plugins/client_snapshot.ts#L161-L173)); and the Preact transform excludes `node_modules` and the Deno npm cache ([`mod.ts` lines 236-244](https://jsr.io/@fresh/plugin-vite/1.0.8/src/mod.ts#L236-L244)). NetScript adds Preact/Signals dedupe and configured watch paths (`packages/fresh/src/application/vite/vite.ts:301-402`), but watch registration does not regenerate island specifiers. The scaffold uses `^1.0.8`, not an exact 1.0.8 pin (`packages/cli/src/kernel/adapters/scaffold/fresh-adapter.ts:145`). +Finding: A dependency island may build once its complete graph is cached, but clean-cache production resolution, Preact transform/dedupe, dependency CSS, local-source HMR, JSR-installed update behavior, and version drift are not proved. The current install-time `deno check` concept does not import the island module and therefore does not close the gap. +Proposal: Make this a Wave-0 blocker gate: generate a static module importing every island; test a clean environment in both local-source and published-JSR modes; run production build plus SSR/hydration; assert one Preact/Signals identity; import dependency CSS; edit a local plugin and observe HMR or a documented full reload; and pin or compatibility-test the plugin-vite range. Do not call the primitive verified until this matrix passes. + +### S-4 Preact component error boundaries do not provide the promised SSR containment [severity: blocker] [area: runtime] +Claim under attack: `design/canonical/04-host-runtime.md:98-118` says every zone is wrapped in `PluginSurfaceBoundary` and that a crashed zone component never takes down the page. +Evidence: Fresh invokes async route components before constructing/rendering the VNode tree ([`render.ts` lines 73-101](https://jsr.io/@fresh/core/2.3.3/src/render.ts#L73-L101)) and renders with `preact-render-to-string` without enabling its optional error-boundary mode (`packages/fresh`'s pinned runtime path corresponds to [`context.ts` lines 420-455](https://jsr.io/@fresh/core/2.3.3/src/context.ts#L420-L455)). The pinned renderer gates boundary catching on `options.errorBoundaries` (`preact-render-to-string@6.7.0/src/index.js:421-425`). A focused pinned-version experiment confirmed that both a synchronous throwing child and an async throwing child escape a class boundary under Fresh's default rendering mode. +Finding: The promised per-zone SSR blast-radius guarantee is false. A broken zone render can fail the entire response; an async component can fail before a boundary even exists. A client-side Preact boundary only helps after hydration. +Proposal: Downgrade the contract to client-render containment unless an upstream-supported SSR mechanism is proven. Catch contribution resolution/data loading before rendering, use Fresh route-level `onError` for route failures, and add a failing SSR fixture as a mandatory gate. If hard server-side zone isolation is required, design an explicit isolated render protocol (with its serialization, context, streaming, and hydration costs) instead of claiming a component boundary supplies it. + +### S-5 `definePluginPage` cannot live in the framework-free contract package or expose the shown context [severity: blocker] [area: contracts] +Claim under attack: `design/canonical/01-contracts.md:24-28` requires `@netscript/plugin-frontend-contracts` to have no Fresh/Preact dependency, while `02-authoring-dx.md:70-89,116-118` imports `definePluginPage` from that package as thin sugar over `define.page()` and supplies `ctx.host` and `ctx.redirect`. +Evidence: Fresh `PageProps` contains request URL, route params, data, state, and related render fields, but no `host` or `redirect` member ([`render.ts` lines 52-66](https://jsr.io/@fresh/core/2.3.3/src/render.ts#L52-L66)). `define.page()` is a Fresh-specific type/helper. The proposed `pluginApi()` helper in the same core import (`02-authoring-dx.md:93-105`) is also runtime behavior, not a framework-free value contract. +Finding: The API cannot be implemented with the claimed dependency boundary or callback shape. Saying that ordinary `define.page()` has the same `ctx.host` access is also false; at most it can read an injected value from `props.state`. +Proposal: Keep the contracts package to serializable types and schemas. Put `definePluginPage`, `pluginApi`, route-module normalization, and Fresh state augmentation in `@netscript/fresh/plugins`. Define the actual callback against Fresh `PageProps`/handler context, or inject a documented `pluginHost` field into application state. Include compile-time author examples using the real signature before freezing it. + +### S-6 The generic proxy is an ambient-authority and request-smuggling boundary [severity: blocker] [area: runtime] +Claim under attack: `design/canonical/04-host-runtime.md:65-79` proposes `/api/plugins/<plugin-id>/*`, resolves a service, forwards session/auth headers, and treats SSE as automatic passthrough; `01-contracts.md:44-48` treats `requires.procedures` as an audit declaration. +Evidence: The existing specialized AI proxy deliberately authenticates server-side, constrains a known upstream path, propagates abort, uses manual redirect handling, and rebuilds a small response-header set (`packages/fresh/src/runtime/ai/stream-proxy.ts:55-67,159-211`). A generic `fetch()` proxy that forwards caller/session headers has to address cookie versus bearer authority, `Host` and hop-by-hop headers, redirects, `Set-Cookie`, CSRF/origin checks, body and response limits, timeouts, abort, upstream URL traversal, response content types, cache policy, and audit logging. The proposed procedure list is not connected to runtime authorization. +Finding: Any plugin frontend could use the host as a confused deputy to reach arbitrary paths on an allowed service, and blindly forwarded credentials or response headers can cross trust boundaries. SSE transport working at the byte-stream level does not make authorization, header hygiene, or durable reconnect semantics correct. +Proposal: Do not ship a wildcard service proxy. Generate a deny-by-default gateway from versioned procedure metadata: service owner, method, path template, request/response mode, required capabilities, and streaming policy. Authenticate/authorize server-side, never forward browser credentials by default, apply CSRF/origin and header allowlists, set redirect to manual, enforce limits/timeouts/abort, and log plugin/procedure/subject. Keep durable AI streaming on its specialized adapter until the generic contract demonstrably covers its semantics. + +### S-7 The base schema cannot evolve or host dashboard-family contributions as claimed [severity: blocker] [area: contracts] +Claim under attack: `design/canonical/01-contracts.md:31-58,239-243` fixes `surfaces` to route/island/zone/nav/theme and calls new optional fields, new zones, and new contribution kinds additive within v1; `design/examples/dashboard.md:72-80` says one manifest emission/registry handshake can add dashboard panels, metric producers, filters, and alert providers. +Evidence: A new member of a discriminated union breaks exhaustive TypeScript consumers and is rejected by older strict validators; it is not backward-compatible merely because the new producer field is optional. The existing plugin schema is strict and versioned, so older readers also reject an added frontend block (`packages/plugin/src/config/validators/manifest-schema.ts`). Dashboard's producer/filter/alert kinds are absent from the base `FrontendManifest` surface shape and cannot pass its stated validation without an untyped escape or a second schema. +Finding: The schema-evolution rule is unsound, and D3's family extension is not representable by D1. This would either block dashboard implementation or quietly weaken the base schema into `unknown[]`, undermining quarantine and compatibility. +Proposal: Separate the stable envelope from family payloads: include `contract: { family, major }` and dispatch to a registered family schema, or give dashboard its own pointer/manifest and registry. Treat any new base discriminant as a major-version event unless v1 defines an extension envelope from day one. Specify old-host/new-plugin and new-host/old-plugin negotiation tests. + +### S-8 Package name, canonical plugin id, installation id, and route namespace are conflated [severity: major] [area: contracts] +Claim under attack: `design/canonical/01-contracts.md:39-40` requires `frontend.plugin` to equal the owning installer manifest name, while every worked manifest uses short values such as `auth`, `ai`, and `crons`; the same value keys routes, CSS scopes, proxy paths, and generated registries. +Evidence: The actual auth installer manifest is named `@netscript/plugin-auth`, while its official source has a separate `canonicalName: "auth"` (`plugins/auth/scaffold.plugin.json:3,50`); AI follows the same pattern. Current install input can also supply a user-facing plugin name/config identity, so package identity is not automatically an instance namespace. +Finding: The examples violate the normative contract, and the design has no stable answer for aliases, forks, or multiple configured instances. Using one ambiguous string for provenance, collision detection, CSS selectors, URLs, and service authorization creates both migration and security problems. +Proposal: Model the identities explicitly: immutable package/source identity, canonical plugin kind, installation/instance id, and host-assigned mount id. Validate each for its intended namespace and derive CSS/proxy/route keys only from the host-assigned id. Update examples and collision diagnostics to use those fields consistently. + +### S-9 `PluginHostState` mixes server capabilities with browser data and imports an optional auth model [severity: major] [area: contracts] +Claim under attack: `design/canonical/04-host-runtime.md:81-96` defines a single state with `session: SessionClaims | null`, a callable `serviceUrl()`, `pluginBase`, `locale`, and `requestId`; `01-contracts.md:150-155` passes it to zones. +Evidence: Functions cannot cross Fresh's island-props serialization boundary, while session claims and locale commonly do. `SessionClaims` is owned by the optional auth family, contradicting a framework-free base contract and hard-wiring one authentication representation. Fresh's CSP nonce is internal request state ([`context.ts` lines 420-455](https://jsr.io/@fresh/core/2.3.3/src/context.ts#L420-L455)); the proposed state has no CSP/useCSP seam, text direction, timezone, host base path, capability/authorization result, or distinction between trusted server claims and safe client claims. +Finding: Authors cannot tell what is server-only, what is serializable, or what is safe to expose to an island. Base-path composition and CSP-compliant script/style behavior remain ad hoc, and unauthenticated hosts acquire an auth-package dependency. +Proposal: Split `PluginRequestContext` (server-only service resolver, authenticated principal port, CSP policy/nonce accessor, abort signal, logger) from a versioned serializable `PluginClientContext` (safe subject summary, locale, direction, timezone, host base path, plugin mount path, capabilities, request correlation). Nest the request context under Fresh app state in the Fresh adapter; let auth supply a port rather than a concrete base-contract type. + +### S-10 Open zones and ambiguous navigation erase the validation the manifest is meant to provide [severity: major] [area: contracts] +Claim under attack: `design/canonical/01-contracts.md:133-176` defines four scaffold-specific zones plus `(string & {})`, permits apps to remove placements, and makes a nav target a string that may be a route id or an absolute path. +Evidence: An open-string union accepts typos and unknown host zones, so install-time validation cannot distinguish a valid but unavailable surface from an invalid contribution. The four `app.*` zones contradict the claimed host-agnostic core, and an overloaded nav string cannot be unambiguously checked, base-path composed, external-link secured, or used to express disabled/capability-gated entries. +Finding: Thinness has been bought by discarding useful static/runtime guarantees. A plugin installed into a host without its preferred zone is not necessarily broken and should not be quarantined, while a misspelled zone should be. Navigation has the same ambiguity and lacks accessibility/external-link semantics. +Proposal: Let each host publish a versioned `HostSurfaceDescriptor` containing zone ids, nav groups, route namespaces, supported contract families, and capabilities. Diagnose unknown, known-but-unmounted, and capacity-rejected contributions separately. Use discriminated nav targets (`route`, `href`, `external`) with base-path generation, capability policy, accessible label/message id, and safe external-link defaults. + +### S-11 Registry generation is incomplete, non-transactional, and not clean on removal [severity: major] [area: discovery] +Claim under attack: `design/canonical/03-discovery-and-registry.md:34-81,103-120` calls four generated files complete, says `plugin uninstall` removes all generated entries, and relies on the existing install type-check gate to quarantine failures. +Evidence: The sketched registry imports only manifests while routes/islands remain strings, so `deno check` need not resolve their deep exports. Current emission writes only files returned by emitters (`packages/plugin/src/sdk/discovery/registry-emitter.ts:10-18`; `packages/cli/src/public/features/generate/plugins/generate-plugin-registries-command.ts:93-102`) and does not inherently delete an axis file that becomes empty. The actual command is `plugin remove`, not `plugin uninstall` (`packages/cli/src/public/features/plugins/remove/remove-plugin-command.ts:39-69`), and removal currently deletes plugin-owned generated directories but not shared registry axis files (`packages/cli/src/public/features/plugins/remove/remove-plugin.ts:74-89`). The install flow does not yet regenerate this frontend set or check all referenced modules (`packages/cli/src/public/features/plugins/install/install-plugin.ts:98-201`). +Finding: Stale registry/CSS/island imports can survive uninstall or a manifest-axis removal; a bad deep export can pass manifest validation and fail only at Vite build; and a failed generation can leave a partially updated host. Copy-mode starter files further need a clear provenance rule because they are intentionally not uninstall-owned runtime contributions. +Proposal: Define a complete replace-set and always emit deterministic empty files for every frontend axis. Stage all outputs, run schema checks plus a generated static import/type-check module, then atomically replace or roll back. Regenerate after install/update/remove, add orphan/stale-output doctor checks, and test local-source, JSR, copied-starter, axis-removal, and full-uninstall cases. Correct the CLI terminology in the design. + +### S-12 CSS scoping does not cover cascade order, portals, or dependency assets [severity: major] [area: runtime] +Claim under attack: `design/canonical/04-host-runtime.md:121-139` says generated imports put `@layer ns-plugins` below app CSS and a `[data-ns-plugin]` wrapper mechanically scopes plugin theme CSS; `03-discovery-and-registry.md:73-81` treats direct and fallback-copied CSS as equivalent. +Evidence: Cascade layer priority is established by a declared layer order/first appearance, not by the layer's name; unlayered author CSS and selector specificity also affect the result. A wrapper cannot scope portals/dialogs rendered outside it, and the route layout needed to provide the wrapper is not represented in the route contract. Copying CSS changes the base URL for relative `url()` assets, so fallback copying is not mechanically equivalent to importing the package export. No asset contribution, hashing, cache policy, or integrity/provenance mechanism exists. +Finding: Plugins can leak styles or silently lose them, overlays may miss theme variables, and fonts/images can break after fallback copying. The promised CSS precedence and stable production caching are unspecified rather than enforced. +Proposal: Emit one host-owned layer-order prelude, define route and zone scope roots, provide a host overlay/portal root per plugin, and lint selectors/keyframes against a documented policy. Add a package-exported asset/module-URL contract or an explicit `AssetContribution` with content hashing and cache headers; test `url()` rewriting and CSP. Treat copied CSS as a separately transformed artifact, not equivalent bytes. + +### S-13 The auth example is designed against APIs that do not exist [severity: blocker] [area: examples] +Claim under attack: `design/examples/auth.md:38-102` requires `auth.org.*`, uses `ctx.host.session.orgId`, and calls `createAuthClient(...).org.members.list()` to implement an organization member console. +Evidence: The current v1 auth contract exposes only sign-in, callback, sign-out, session, and me (`packages/plugin-auth-core/src/contracts/v1/auth.contract.ts:410-459`). Repository search finds no `auth.org.*` procedure family or matching organization-members client. Session claims provide a generic claims record, not the specified `orgId` field (`auth.contract.ts:74-88`). +Finding: The flagship interactive example cannot compile or run against the cited backend. It also makes an organization capability look guaranteed when neither the procedure nor a versioned claim/capability exists. +Proposal: Make the v1 live plugin cover the real five auth procedures and current session surface. Move organization administration to a future, separately versioned auth-org capability; specify its procedures and safe client claim/capability before showing UI. Keep the copied starter limited to app-owned branding/composition, not invented backend functionality. + +### S-14 The AI example conflates two different streaming architectures [severity: major] [area: examples] +Claim under attack: `design/examples/ai.md:45-71` points the existing Fresh AI runtime at `/api/plugins/ai/chat` and treats a generic proxy plus a distributed worker import as the complete live story. +Evidence: The current `createNetScriptChatConnection` requires a durable-session `target` (including a session id), not an arbitrary endpoint string (`packages/fresh/src/runtime/ai/create-chat-connection.ts:378-424`). Its `/ai/chat` transport uses a specialized proxy with authentication, path construction, abort, redirect, and response-header rules (`packages/fresh/src/runtime/ai/stream-proxy.ts:159-211`). The AI contract also has an oRPC event-iterator streaming surface (`packages/plugin-ai-core/src/contracts/v1/ai.contract.ts:317-391`), which is a distinct client model. +Finding: The example promises reuse that the actual API does not support and leaves authorization/reconnect/session semantics undefined. Requiring the workers frontend module also risks coupling a browser contribution to a backend deployment adapter rather than to a stable AI capability. +Proposal: Choose and document one v1 architecture: either an oRPC/event-iterator client generated from the AI contract, or the durable-session runtime with a generated specialized route and authorization hook. Express the worker requirement as a backend capability/procedure requirement, not a browser module import, and add reconnect/cancel/partial-token tests. + +### S-15 The deploy example omits required operations and reverses the adapter dependency direction [severity: major] [area: examples] +Claim under attack: `design/examples/deploy.md:9-13,45-57` defines a uniform five-operation contract and has deploy core lazily import target-specific Azure/Docker/Kubernetes islands. +Evidence: The governing deploy-target archetype defines seven uniform operations: plan/emit, up, down, status, logs, rollback, and secrets (`.llm/harness/archetypes/ARCHETYPE-7-deploy-target-adapter.md:50-63`). Its law is core contracts plus target adapters, with parity across adapters. A core frontend importing target modules creates knowledge of every adapter in core and makes third-party targets require a core change. +Finding: The example is already contract-incomplete, and its module graph violates the adapter boundary it claims to showcase. Lazy loading reduces bytes but does not repair reversed ownership. +Proposal: Include all seven operations or explicitly version a reduced read-only v1. Let each target adapter contribute its own panel/islands through a deploy-family zone/registry and declare which operation capabilities it supports; deploy core should consume the registry and stable contract without importing targets. Add an adapter parity fixture. + +### S-16 The advertised authoring loop hides serialization, exports, and regeneration work [severity: major] [area: dx] +Claim under attack: `design/canonical/02-authoring-dx.md:15-18,70-149` calls the path minimal, shows islands taking arbitrary object props, promises watch/HMR for local plugins, and defers automatic `deno.json` export maintenance to Phase 2. +Evidence: Fresh island props must be serializable across the server/client boundary; the design supplies no props schema, serialization-safe type, or diagnostic. JSR package consumers can resolve only published entrypoints, and the documented JSR multi-entrypoint model requires maintaining the export map ([JSR publishing packages](https://jsr.io/docs/publishing-packages#multiple-entrypoints)). NetScript's Vite watch path only adds filesystem paths to the watcher (`packages/fresh/src/application/vite/vite.ts:397-402`); it does not rerun frontend registry generation when a manifest changes, while plugin-vite's island watcher does not watch dependency manifests/specifier lists. +Finding: The happy path is short only after authors manually maintain exports, restart/regenerate at the right moments, and discover serialization failures late. Local-source and JSR-installed plugins have materially different edit loops, but the design presents one seamless loop. +Proposal: Put export-map maintenance and generated static loader imports in Phase 1. Provide `Serializable`/schema-backed island props and a check that rejects functions, request objects, and server-only state. Add `netscript plugin dev` (or equivalent task) that watches the manifest/export map, regenerates atomically, and signals Vite restart/full reload when island inputs change. Document immutable JSR mode separately from live local-source mode. + +### S-17 Internationalization, accessibility, CSP, and responsive behavior need contract seams, not prose later [severity: major] [area: missing] +Claim under attack: `design/canonical/01-contracts.md:150-176` exposes raw English `title`/`label` strings and only a locale string in host state; the plan and risk register contain no i18n, accessibility, CSP, mobile, or reduced-motion gate. +Evidence: Plugin strings cannot be host-localized without message identity/catalog ownership and fallback rules. Locale alone omits direction and timezone. Fresh's nonce is request-scoped internal state ([`context.ts` lines 420-455](https://jsr.io/@fresh/core/2.3.3/src/context.ts#L420-L455)), while islands, styles, external assets, and service origins all interact with CSP. Nav/zone contributions also need keyboard order, landmarks/headings, focus restoration, accessible fallback status, responsive width constraints, and reduced-motion behavior. +Finding: These concerns affect the shape of `NavContribution`, `ZoneProps`, client context, assets, and error/loading UI. Deferring all of them would force breaking contract changes or host-specific conventions after plugins ship. +Proposal: Add localized message references with default text and plugin-owned catalogs; serialize locale/direction/timezone; define CSP/nonce/network-origin and asset rules; add zone size/responsiveness metadata only where hosts truly need it; and publish accessibility requirements for nav, loading, empty, error, focus, and live streaming states. Gate examples with automated axe plus keyboard/mobile/reduced-motion scenarios. + +### S-18 There is no end-to-end frontend plugin quality or performance contract [severity: major] [area: missing] +Claim under attack: `plan.md:47-69` names check/lint/scaffold gates, but neither the plan nor canonical design defines plugin-level SSR/hydration/browser tests, performance budgets, or versioned asset-cache verification. +Evidence: `deno check` cannot prove Vite chunking, SSR rendering, hydration identity, CSP behavior, browser errors, CSS order, SSE cancellation, or uninstall cleanliness. Lazy route loading does not make zones lazy: zones rendered on a page can add SSR work and island chunks together. No bound exists for JS/CSS bytes, island count, route/zone render time, parallel backend calls, cache lifetime, or fallback latency. +Finding: A plugin can pass every proposed gate while breaking production hydration, bloating every dashboard page, or hanging SSR. Four initial consumers will create precedent before the host has a way to measure or reject regressions. +Proposal: Provide a generated host-fixture test kit with schema/resolution, SSR, hydration, browser, a11y, CSP, base-path, local/JSR, and install/remove tests. Record per-plugin production manifest budgets (initial JS, async chunks, CSS, island count), server render/fetch deadlines, cancellation, and cache rules; make exceptions explicit host policy rather than undocumented convention. + +### S-19 Wave 1 freezes public contracts before proving the mechanisms they encode [severity: major] [area: plan] +Claim under attack: `plan.md:47-51` puts contracts, plugin schema/DSL, CLI, host runtime, Vite, scaffold, and first dogfood in one wave, while `plan.md:74` leaves island behavior as a spike/risk and `plan.md:64-69` relies heavily on broad downstream gates. +Evidence: S-1 through S-6 invalidate or leave unproved the route, layout, middleware, island, containment, author-helper, and proxy mechanisms that determine the public contract. The full scaffold runtime gate is appropriate for merge readiness but is too broad and expensive to guide each mechanism decision; the harness doctrine calls for contract/gate evidence before implementation expansion. +Finding: The first wave is too wide to localize failures and is ordered backward: it can stabilize a public manifest around mechanisms that later require breaking changes. The security-sensitive proxy is especially unsuitable as incidental infrastructure inside the same wave. +Proposal: Insert Wave 0 with five disposable proofs: mounted-app ordering/layout/base-path; normalized literal lazy routes; local+JSR clean-cache island build/HMR/CSS/dedupe; SSR failure containment; and gateway threat model/stream abort. Then slice Wave 1 as envelope/schema plus generated static module, one route, one island, and transactional install/remove. Add zones/nav/theme only after host-surface negotiation; build the gateway as its own reviewed wave. Keep `scaffold.runtime` for final convergence and use focused fixtures per slice. + +### S-20 Several owner forks are technical invariants or unresolved drift, not preference choices [severity: major] [area: plan] +Claim under attack: `design/canonical/05-owner-forks.md` presents additive-minor new zones/kinds (F4), Phase-2 export generation (F6), AI plus workers dogfood (F7), and route-base collision policy (F2) as choices; `design/canonical/00-overview.md:48-56` permits both manifest pointers and DSL declaration. +Evidence: S-7 shows new union kinds are not additive to old validators; S-16 shows explicit published entrypoints are required for the proposed module model; S-14 shows the AI/worker dependency is not the actual frontend API. Exact base-string collision checks also miss overlapping route patterns, reserved host paths (`/_fresh`, `/api/plugins`), nested/dynamic route precedence, and host `basePath`. Two independently authored pointers can drift even if both are individually valid. +Finding: Asking the owner to choose among technically incompatible states risks blessing a broken contract. Conversely, the genuine product choices—default mount behavior, visual token governance, starter boundaries, and whether hard SSR zone isolation is worth its cost—are obscured by questions that evidence can settle. +Proposal: Resolve F4, F6, and the F7 mechanism from proof/contract facts before owner review. Expand F2 into deterministic full-route-pattern/reserved-path/base-path collision rules. Establish one canonical frontend declaration source and generate/verify any DSL/installer projection from it. Return only genuine policy forks to the owner, with the Wave-0 evidence and explicit compatibility consequences. + +## Verified-claims log +- CONFIRMED — Fresh 2.3.3 `App.mountApp()` prefixes child route/middleware/layout paths with the child base path and mount path and shares the build cache; child `App`/`NotFound` commands are not path-scoped ([source](https://jsr.io/@fresh/core/2.3.3/src/app.ts#L353-L385)). +- CONFIRMED — Fresh 2.3.3 `App.route()` accepts a lazy internal `Route`, but filesystem module normalization is a separate fs-routes operation ([route types](https://jsr.io/@fresh/core/2.3.3/src/types.ts#L55-L72), [fs normalization](https://jsr.io/@fresh/core/2.3.3/src/fs_routes.ts#L143-L176)). +- CONFIRMED — Fresh plugin-vite 1.0.8 adds configured `islandSpecifiers` as Rollup inputs and client-snapshot dynamic imports ([source](https://jsr.io/@fresh/plugin-vite/1.0.8/src/plugins/client_snapshot.ts#L119-L140)). +- CONFIRMED — Fresh plugin-vite 1.0.8 uses cached-only Deno resolution/loading and its automatic island watcher is local-directory based ([resolver](https://jsr.io/@fresh/plugin-vite/1.0.8/src/plugins/deno.ts#L40-L51), [watcher](https://jsr.io/@fresh/plugin-vite/1.0.8/src/plugins/client_snapshot.ts#L161-L173)). +- CONFIRMED — NetScript's Vite plugin deduplicates/canonicalizes Preact and Signals and can add plugin paths to Vite's watcher (`packages/fresh/src/application/vite/vite.ts:301-402`); it does not regenerate frontend inputs by itself. +- CONFIRMED — JSR's documented multiple-entrypoint model uses package export-map entries, so published deep frontend modules need maintained exports ([JSR documentation](https://jsr.io/docs/publishing-packages#multiple-entrypoints)). +- CONFIRMED — The live auth v1 contract contains sign-in, callback, sign-out, session, and me, not organization-member procedures (`packages/plugin-auth-core/src/contracts/v1/auth.contract.ts:410-459`). +- CONFIRMED — NetScript has both an AI event-iterator contract and a separate durable-session Fresh chat runtime/specialized proxy (`packages/plugin-ai-core/src/contracts/v1/ai.contract.ts:317-391`; `packages/fresh/src/runtime/ai/create-chat-connection.ts:378-424`). +- CONFIRMED — The deploy-target adapter archetype requires plan/emit, up, down, status, logs, rollback, and secrets (`.llm/harness/archetypes/ARCHETYPE-7-deploy-target-adapter.md:50-63`). From 53e428acc05ee41a8114cc0eaf9e4ff04fea2d64 Mon Sep 17 00:00:00 2001 From: Rickylabs <eric.chautems@gmail.com> Date: Sun, 19 Jul 2026 06:25:59 +0200 Subject: [PATCH 04/11] =?UTF-8?q?plan(frontend-contrib):=20integrate=20Sol?= =?UTF-8?q?=20adversarial=20findings=20=E2=80=94=20rev=202=20(all=2020=20a?= =?UTF-8?q?ccepted)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Envelope+family versioning, identity quartet, context split, HostSurfaceDescriptor, literal route loaders + post-fsRoutes composition, islands proof-gated, honest SSR containment, deny-by-default procedure gateway, transactional emissions, corrected consumer examples, Wave-0 five-proof phasing, fork re-triage. Triage dispositions in adversarial-triage.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015fc7SaneZJ9mzrAhRW4tma --- .../adversarial-triage.md | 33 ++ .../codex-thread-ids.md | 16 + .../context-pack.md | 12 +- .../design/canonical/00-overview.md | 17 +- .../design/canonical/01-contracts.md | 347 ++++++++++-------- .../design/canonical/02-authoring-dx.md | 151 ++++---- .../canonical/03-discovery-and-registry.md | 163 ++++---- .../design/canonical/04-host-runtime.md | 275 +++++++------- .../canonical/05-scaffolding-and-cli.md | 26 +- .../design/canonical/06-doctrine-fit.md | 5 +- .../design/examples/ai.md | 21 +- .../design/examples/auth.md | 108 +++--- .../design/examples/deploy.md | 35 +- .../runs/plan-frontend-contrib--seed/drift.md | 1 + .llm/runs/plan-frontend-contrib--seed/plan.md | 158 ++++---- .../plan-frontend-contrib--seed/worklog.md | 21 ++ 16 files changed, 738 insertions(+), 651 deletions(-) create mode 100644 .llm/runs/plan-frontend-contrib--seed/adversarial-triage.md create mode 100644 .llm/runs/plan-frontend-contrib--seed/codex-thread-ids.md diff --git a/.llm/runs/plan-frontend-contrib--seed/adversarial-triage.md b/.llm/runs/plan-frontend-contrib--seed/adversarial-triage.md new file mode 100644 index 000000000..d91ec61de --- /dev/null +++ b/.llm/runs/plan-frontend-contrib--seed/adversarial-triage.md @@ -0,0 +1,33 @@ +# Adversarial Triage — per-finding dispositions (generator, stage 2 integration) + +Verdict on the review: **high quality; accepted with minor citation corrections.** The reviewer +verified upstream claims against jsr sources and repo code; its seven blockers are real design +flaws, not taste. Two slips noted (S-20 cites a non-existent `05-owner-forks.md` — forks live in +`plan.md`; S-15 misnames my example's adapters as Azure/Docker/K8s — they are cloudflare/aws — +and calls the op set five-op where the example listed six) — neither changes the substance. + +| # | Sev | Disposition | Integration | +| --- | --- | --- | --- | +| S-1 mount ordering | blocker | **ACCEPT** — insertion-order compilation + `configure()`-before-fsRoutes means my sketch broke both plugin middleware and host layout wrap | 04 §2 rewritten: strict register order (middleware→layout→routes), mount in a post-fsRoutes composition phase, child `App`/`NotFound` stripped; Wave-0 proof P1 | +| S-2 route normalization | blocker | **ACCEPT** — `App.route()` takes internal `Route`, not a fs route module | literal generated loaders + `normalizeFreshRouteModule` adapter owned by `@netscript/fresh/plugins`; plugin `_layout` forbidden in v1; 01/03/04 updated; Wave-0 proof P2 | +| S-3 islands unproven | blocker | **ACCEPT** — API exists (still true) but cachedOnly resolve, Preact-transform excludes, watcher scope, CSS, HMR are unproved | reclassified from "verified primitive" to "verified API, unproven behavior"; Wave-0 proof P3 with the reviewer's full matrix; plugin-vite pin/compat test | +| S-4 SSR containment | blocker | **ACCEPT** — preact-render-to-string error-boundary mode not enabled by Fresh; async components fail earlier anyway | containment contract downgraded: host-side data resolution catch + route `onError` + client-boundary; SSR zone-throw = page error documented; Wave-0 proof P4 | +| S-5 sugar placement | blocker | **ACCEPT** — runtime helpers cannot live in the framework-free contracts package | `definePluginPage`/`pluginApi`/normalizer moved to `@netscript/fresh/plugins`; real signature over Fresh `PageProps` + state-injected `pluginHost`; 01/02 updated | +| S-6 proxy security | blocker | **ACCEPT** — wildcard forwarding proxy is a confused-deputy surface | replaced by deny-by-default generated **procedure gateway** (owner/method/path/streaming policy from versioned procedure metadata); AI durable streaming stays on its specialized adapter; 04 §4 rewritten; Wave-0 proof P5 (threat model) | +| S-7 schema evolution | blocker | **ACCEPT** — new union members break strict old validators; dashboard kinds can't ride the base schema | envelope/family model: `contract: { family, major }` + per-family registered payload schemas; new base discriminants = major; negotiation tests specified; 01 rewritten | +| S-8 identity conflation | major | **ACCEPT** — package name ≠ canonical kind ≠ installation id ≠ mount id (`officialSource.canonicalName` precedent) | explicit identity quartet in 01; CSS/proxy/route keys derive from host-assigned mount id; examples aligned | +| S-9 context split | major | **ACCEPT** — functions can't cross island serialization; auth type import breaks framework-free claim | `PluginRequestContext` (server) / `PluginClientContext` (serializable) split; auth becomes a principal **port**; 01/04 updated | +| S-10 zones/nav validation | major | **ACCEPT** — open string union destroys validation; nav string ambiguous | `HostSurfaceDescriptor` (versioned, host-published); unknown vs unmounted vs capacity diagnoses; discriminated nav targets (`route`/`href`/`external`); 01/03 updated | +| S-11 registry lifecycle | major | **ACCEPT** — non-transactional emission, stale files on removal, verb is `plugin remove` | complete replace-set, deterministic empty emissions, staged atomic replace + rollback, regenerate on install/update/remove, orphan doctor check; verb corrected; 03 rewritten §4 | +| S-12 CSS/assets | major | **ACCEPT** — layer order is declaration-order, portals escape scoping, copied CSS breaks `url()` | host-owned layer-order prelude, per-plugin portal root, copy-mode url() caveat; full `AssetContribution` deferred with debt entry; 04 §8/03 §2 updated | +| S-13 auth example | blocker | **ACCEPT** — org procedures don't exist in auth v1 | example rewritten: v1 live = the real 5 procedures (account/session widget/sign-out); org console explicitly future `auth-org` capability with backend contract prerequisite | +| S-14 ai example | blocker→major | **ACCEPT** — durable chat needs session target + specialized proxy | example rewritten: v1 = durable-session runtime route (generated specialized route + auth hook); oRPC event-iterator named as the alternative; assist = capability requirement not module import | +| S-15 deploy example | major | **ACCEPT** — op set is 7 (incl rollback/secrets); core importing adapter islands reverses ownership | example rewritten: adapters contribute panels via deploy-family registry; core consumes registry; 7-op or explicitly versioned read-only v1 | +| S-16 DX hidden work | major | **ACCEPT** — exports maintenance + loader generation are Phase-1; props serializability; split JSR/local loops | 02/05/plan updated; `netscript plugin dev` watcher added; F6 resolved (no longer a fork) | +| S-17 i18n/a11y/CSP | major | **ACCEPT (scoped)** — contract-shape-affecting parts land in v1 shapes; full policies staged | message refs (`{ id, default }`) on nav/titles, locale/direction/timezone in client context, CSP/nonce seam documented, a11y gate list added to plan; deeper policy work = implementation wave | +| S-18 quality/perf | major | **ACCEPT** — no end-to-end plugin frontend test kit or budgets existed | `./testing` host-fixture kit spec + per-plugin budget table added to plan gates + 05 §5 | +| S-19 Wave 0 | major | **ACCEPT** — contracts frozen before mechanism proofs is backwards | plan re-phased: **Wave 0 = five disposable proofs (P1–P5)**, Wave 1 re-sliced narrow, gateway its own reviewed wave | +| S-20 fork re-triage | major | **ACCEPT** — technical invariants removed from fork list | F4/F6 resolved by S-7/S-16; F2 expanded to full route-pattern/reserved-path collision rules; genuine policy forks retained (see plan.md revised F-table) | + +Integration commit(s) follow this file; each doc carries a change note referencing the S-numbers +it absorbs. diff --git a/.llm/runs/plan-frontend-contrib--seed/codex-thread-ids.md b/.llm/runs/plan-frontend-contrib--seed/codex-thread-ids.md new file mode 100644 index 000000000..e2304323a --- /dev/null +++ b/.llm/runs/plan-frontend-contrib--seed/codex-thread-ids.md @@ -0,0 +1,16 @@ +# frontend-contrib-sol — Codex implementation thread +- **Thread / session id:** `019f7883-ccf0-7820-aa36-3bd90b82ac05` +- **Rollout:** `/home/codex/.codex/sessions/2026/07/19/rollout-2026-07-19T05-55-31-019f7883-ccf0-7820-aa36-3bd90b82ac05.jsonl` +- **Worktree:** `/home/codex/repos/wt-frontend-contrib` +- **Branch:** `plan/frontend-contrib` (NO upstream by design). +- **Push rule:** explicit refspec only — `git push origin HEAD:refs/heads/plan/frontend-contrib`. +- **Requested route:** provider=openai · model=gpt-5.6-sol · effort=high +- **Observed route:** provider=openai · model=gpt-5.6-sol · effort=high +- **Route verdict:** matched +- **Runtime:** approval=never · sandbox=dangerFullAccess +- **Brief (staged):** `/home/codex/frontend-contrib-sol-brief.md` +## Steering (same thread — never a second send-message-v2 at this worktree) +```bash +codex exec resume 019f7883-ccf0-7820-aa36-3bd90b82ac05 -- "<follow-up>" +``` +_Written by `.llm/tools/agentic/codex/launch-codex-slice.ts`._ \ No newline at end of file diff --git a/.llm/runs/plan-frontend-contrib--seed/context-pack.md b/.llm/runs/plan-frontend-contrib--seed/context-pack.md index 59b3e7b3a..731efeac8 100644 --- a/.llm/runs/plan-frontend-contrib--seed/context-pack.md +++ b/.llm/runs/plan-frontend-contrib--seed/context-pack.md @@ -10,7 +10,17 @@ plugin. Drafts only: no PRs/issues, no product code. Custom pipeline: this gener GPT-5.6 Sol high adversarial pass → generator integrates → Kimi K3 public docs/API pass. All downstream stages are supervisor-dispatched. -## State: GENERATOR STAGE COMPLETE +## State: STAGE 2 (ADVERSARIAL) INTEGRATED — awaiting Kimi K3 docs pass + +Sol high review (thread `019f7883-ccf0-7820-aa36-3bd90b82ac05`) produced 20 findings +(`adversarial-sol.md`); all accepted and integrated as **rev 2** of plan + canonical docs + +examples (`adversarial-triage.md` has per-finding dispositions). Headline rev-2 changes: +envelope+family versioning (no widened unions), identity quartet, request/client context split, +HostSurfaceDescriptor, literal route loaders + post-fsRoutes composition, islands proof-gated +[P3], honest SSR containment, deny-by-default procedure gateway (no wildcard proxy), Wave-0 +five-proof phasing, forks re-triaged (F9 new). Examples now compose only real backend surfaces. + +## Original generator state (stage 1) All deliverables written and committed on `plan/frontend-contrib`: diff --git a/.llm/runs/plan-frontend-contrib--seed/design/canonical/00-overview.md b/.llm/runs/plan-frontend-contrib--seed/design/canonical/00-overview.md index 2175e6c41..4698108f8 100644 --- a/.llm/runs/plan-frontend-contrib--seed/design/canonical/00-overview.md +++ b/.llm/runs/plan-frontend-contrib--seed/design/canonical/00-overview.md @@ -1,8 +1,10 @@ # Frontend Contribution Layer — Overview (canonical design, v1 draft) > **Draft — design document only. No GitHub mutations, no product code.** Produced by -> `plan-frontend-contrib--seed`; pending adversarial (GPT-5.6 Sol) and docs (Kimi K3) passes and -> owner ratification. +> `plan-frontend-contrib--seed`. Rev 2: the GPT-5.6 Sol adversarial pass is integrated (20/20 +> findings accepted — `../../adversarial-sol.md`, dispositions in `../../adversarial-triage.md`); +> pending the Kimi K3 docs pass and owner ratification. Mechanisms marked [P1]–[P5] in the docs +> are Wave-0 proof gates (`../../plan.md`) — verified-API-unproven-behavior is stated as such. ## The one-paragraph story @@ -54,11 +56,12 @@ that Fresh, the plugin SDK, or the fresh-ui token system doesn't already provide (`plugins/ai/src/adapter/resources/chat-route/chat-route.ts:33`). For surfaces the user is *expected to customize* (a sign-in page, a cloud-optimized starter route). The rule of thumb: **if the user will edit it, scaffold it; if the plugin owns it, serve it.** -6. **One base family, many hosts.** The user app and the dev dashboard are both hosts. The base - contract family (routes, islands, zones, nav, theme) is host-agnostic; the dashboard's richer - kinds (entity tabs, ⌘K actions, AI tools, home cards) are a *family extension* owned by the - future `plugin-dashboard-core`, not a sibling vocabulary. This generalizes — rather than - duplicates — the ratified dashboard architecture. +6. **One envelope, many families, many hosts.** The user app and the dev dashboard are both + hosts. The `app` family (routes, islands, zones, nav, theme) and the dashboard's richer + family (entity tabs, ⌘K actions, AI tools, home cards — future `plugin-dashboard-core`) are + **sibling payload families on one shared envelope**, sharing discovery, identity, and + host-surface negotiation (rev 2, S-7: a widened union is not additively evolvable). This + generalizes — rather than duplicates — the ratified dashboard architecture. 7. **Thinness preserved, axis first-class.** `@netscript/plugin` gains only a *pointer* (`frontend: { export, framework, contract }`) — the same shape as `scaffolder.export` in the installer manifest. The typed vocabulary lives in `plugin-frontend-core`. This resolves diff --git a/.llm/runs/plan-frontend-contrib--seed/design/canonical/01-contracts.md b/.llm/runs/plan-frontend-contrib--seed/design/canonical/01-contracts.md index 7e72b4d35..f2afc0be2 100644 --- a/.llm/runs/plan-frontend-contrib--seed/design/canonical/01-contracts.md +++ b/.llm/runs/plan-frontend-contrib--seed/design/canonical/01-contracts.md @@ -1,243 +1,268 @@ -# Contracts — `@netscript/plugin-frontend-core/contracts/v1` (draft) +# Contracts — `@netscript/plugin-frontend-core/contracts/v1` (draft, rev 2) -> **Draft — design document only.** Shapes are normative in intent, sketch in detail; field-level -> bikeshedding belongs to the adversarial + docs passes. Idiom follows -> `BasePluginContract` (`packages/plugin/src/contract-base/domain/base-contract.ts`): typed seams, -> Standard-Schema (zod) validation, re-exported — never redefined — by consumers. +> **Draft — design document only.** Rev 2 integrates adversarial findings S-2, S-5, S-7, S-8, +> S-9, S-10, S-17 (`../../adversarial-sol.md`, dispositions in `../../adversarial-triage.md`). +> Idiom follows `BasePluginContract`: typed seams, Standard-Schema (zod) validation, re-exported — +> never redefined — by consumers. ## Package `@netscript/plugin-frontend-core` — new **Archetype 1 (small contract)** package. Exports: ```jsonc -// packages/plugin-frontend-core/deno.json (sketch) { "name": "@netscript/plugin-frontend-core", "exports": { ".": "./mod.ts", // defineFrontend + re-export of contracts/v1 "./contracts/v1": "./src/contracts/v1/mod.ts", - "./testing": "./src/testing/mod.ts" // renderContribution harness (phase 2) + "./testing": "./src/testing/mod.ts" // host-fixture test kit (see plan §Gates) } } ``` -Dependency posture: depends on nothing but `@netscript/contracts`-grade utilities and zod. -It must **not** depend on `fresh`, `@netscript/fresh`, or `@netscript/fresh-ui` — contributions -are data; only hosts touch the framework. (Component *references* are module specifiers, not -component values — see `ComponentRef` below. This is what keeps the contract package -framework-free and the registry statically emittable.) +**Dependency law (S-5 hardened):** this package holds **serializable types, schemas, and the +`defineFrontend` validator only**. No `fresh`, no `@netscript/fresh`, no `preact`, no runtime +helpers. Everything that touches the framework — `definePluginPage`, `pluginApi`, the route-module +normalizer, state injection — lives in `@netscript/fresh/plugins` (`04-host-runtime.md`). +Component *references* are module specifiers (data), which is what keeps this package +framework-free and the registry statically emittable. -## The manifest — what a plugin's `./frontend` export IS +## Identity model (S-8) + +One string cannot serve provenance, URLs, CSS scoping, and authorization. Four identities, each +with one job: ```ts -// contracts/v1/manifest.ts (sketch) - -/** The value of a plugin's `./frontend` export — pure data, validated at build time. */ -export interface FrontendManifest { - /** Contract handshake. The ONLY compat gate (mirrors contributesTo in the dashboard family). */ - readonly contract: 'v1'; - /** Plugin id — must equal the owning plugin's manifest name (validated by the registry). */ - readonly plugin: string; +export interface FrontendIdentity { + /** Immutable package/source identity: '@netscript/plugin-auth'. Provenance + version drift. */ + readonly packageName: string; /** - * Preferred mount base for route contributions, e.g. '/crons'. The HOST may remap - * (04-host-runtime.md §2); collisions are a generate-time error, not a runtime surprise. - * Defaults to '/' + plugin id. + * Canonical plugin kind: 'auth', 'crons' — matches the installer manifest's + * officialSource.canonicalName idiom (plugins/auth/scaffold.plugin.json:50). Stable across + * forks/aliases of the same capability. */ - readonly base?: string; - readonly routes?: readonly RouteContribution[]; - readonly islands?: readonly IslandContribution[]; - readonly zones?: readonly ZoneContribution[]; - readonly nav?: readonly NavContribution[]; - readonly theme?: readonly ThemeContribution[]; - /** Declared data needs — audit surface + doctor input + future T1/T2 enforcement. */ - readonly requires?: FrontendRequires; -} - -export interface FrontendRequires { - /** Core/host ports the contributions read (e.g. 'telemetry-query'). */ - readonly ports?: readonly string[]; - /** Own-contract oRPC procedures called through the plugin API proxy (04 §4). */ - readonly procedures?: readonly string[]; + readonly pluginKind: string; + /** Installation/instance id, host-assigned at install; = pluginKind unless multi-instance. */ + readonly installationId: string; + /** Host-assigned mount id — THE key for routes, CSS scope, gateway paths, registry entries. */ + readonly mountId: string; } ``` -## Component references +Manifests declare `pluginKind`; the installer/registry assigns `installationId`/`mountId` and +validates `packageName` against the owning plugin. All generated keys (base path, `data-ns-plugin` +scope, gateway prefix, typed route ref namespace) derive from `mountId`, never from `packageName`. + +## The envelope — versioning that can actually evolve (S-7) + +A discriminated-union member added to a strict schema is **not** additive: old validators reject +it, exhaustive consumers break. v1 therefore separates a stable **envelope** from **family +payloads**: ```ts -// contracts/v1/refs.ts (sketch) - -/** - * A module specifier RELATIVE TO THE PLUGIN'S `frontend/` directory, e.g. - * './routes/calendar.tsx'. The registry emitter resolves it against the plugin's package name - * to a full importable specifier ('@acme/plugin-crons/frontend/routes/calendar'). - * Data, not code: manifests never hold component values. - */ -export type ComponentRef = `./${string}`; +// contracts/v1/envelope.ts +export interface FrontendManifestEnvelope { + /** Family + major — the handshake. Hosts register family schemas they support. */ + readonly contract: { readonly family: 'app'; readonly major: 1 } | FamilyRef; + readonly pluginKind: string; + /** Preferred mount base ('/crons'); host remaps; full collision rules in 03 §3. */ + readonly base?: string; + /** Family payload, validated by the family's registered schema — never by the envelope. */ + readonly contributions: readonly unknown[]; + readonly requires?: FrontendRequires; +} +export interface FamilyRef { readonly family: string; readonly major: number } ``` -## Contribution kinds (the base family) +- The **`app` family, major 1** is this document's payload schema (the five kinds below). +- The dashboard family is `{ family: 'dashboard', major: 1 }` — its own payload schema in + `plugin-dashboard-core`, its own kinds, validated by hosts that register it. It *extends + nothing at the schema level*; it shares the envelope, discovery pipeline, identity model, and + host-surface negotiation. (This replaces the rev-1 "widened union" story, which was unsound.) +- **Evolution rules:** new optional field on an existing kind = minor (validators must ignore + unknown fields — schemas are `.passthrough()` at the payload boundary); new kind or new + discriminant = **new major** of that family; hosts declare supported `(family, major)` windows + in their `HostSurfaceDescriptor` and quarantine outside the window. Old-host/new-plugin and + new-host/old-plugin negotiation each get a contract test. -All kinds share: +A plugin's `./frontend` export is one envelope (or an array of envelopes, one per family — e.g. a +plugin contributing to both the app surface and the dashboard). + +## `app` family payload — the five kinds + +Shared base: ```ts -// contracts/v1/contribution.ts (sketch) -export interface FrontendContributionBase { - /** Stable id unique within the plugin: 'calendar', 'session-widget'. Full id = '<plugin>/<id>'. */ - readonly id: string; - readonly title?: string; - /** fresh-ui icon name (packages/fresh-ui primitives IconName). */ - readonly icon?: string; - /** - * Which host surfaces may mount this contribution. Default ['app']. - * 'dashboard' reserves the dev-dashboard host; its family extension adds dashboard-only kinds. - */ - readonly surfaces?: readonly ('app' | 'dashboard')[]; +export interface AppContributionBase { + readonly id: string; // unique within (plugin, family) + /** Human title as a message ref: host-localizable, default text mandatory (S-17). */ + readonly title?: MessageRef; + readonly icon?: string; // fresh-ui IconName } +export interface MessageRef { readonly id: string; readonly default: string } ``` -### 1. `RouteContribution` — a page +### 1. `route` ```ts -export interface RouteContribution extends FrontendContributionBase { +export interface RouteContribution extends AppContributionBase { readonly kind: 'route'; - /** Path under the manifest base, Fresh syntax: '/calendar', '/schedules/:id'. */ - readonly path: string; - /** Fresh route module: default export page and/or `handler`, standard `define.*` idiom. */ + readonly path: string; // under the resolved base, Fresh syntax + /** + * Fresh route module by package-relative ref ('./routes/calendar.tsx'). The generated + * registry emits a LITERAL lazy loader per route — + * () => import('@scope/pkg/frontend/routes/calendar').then(normalizeFreshRouteModule) + * — because upstream App.route() takes an internal Route shape, not a fs route module, and + * computed import() hides the edge from the bundler (S-2). normalizeFreshRouteModule is owned + * by @netscript/fresh/plugins and maps default/handler(s)/config/css onto the Route shape. + * Plugin `_layout` modules are NOT supported in v1 (explicitly rejected at generate time). + */ readonly module: ComponentRef; - /** Optional convenience: emit a NavContribution pointing here. */ - readonly nav?: Omit<NavContribution, 'kind' | 'id' | 'route'>; + readonly nav?: NavSpec; // convenience: emits a nav contribution targeting this route } ``` -Route modules are **ordinary Fresh route modules** — `define.page`, `define.handlers`, layouts via -a plugin-local `_layout` (`04-host-runtime.md §2`). Lazy-loaded by the host -(`App.route(path, MaybeLazy)` upstream surface). - -### 2. `IslandContribution` — interactive component +### 2. `island` ```ts -export interface IslandContribution extends FrontendContributionBase { +export interface IslandContribution extends AppContributionBase { readonly kind: 'island'; - /** One island component per file (Fresh convention). All exports of the module hydrate. */ - readonly module: ComponentRef; // './islands/CronCalendar.tsx' + readonly module: ComponentRef; // one island component per file (Fresh convention) } ``` -Islands are registered with the build (`islandSpecifiers` / `registerIsland`) so **plugin pages -and zone components simply import and render them** — the exact idiom app authors already use. -No wrapper, no props schema at v1: types flow through the import. +Island props cross Fresh's serialization boundary: **props must be serializable data** — the +authoring doc states it, the test kit checks it, and `PluginClientContext` (below) defines what +host data is island-safe. Build-behavior proof status: Wave-0 gate P3 (`../../plan.md`) — the +`islandSpecifiers` API is verified; clean-cache/JSR/HMR/CSS behavior is not yet. -### 3. `ZoneContribution` — SSR component into a host injection zone +### 3. `zone` ```ts -export interface ZoneContribution extends FrontendContributionBase { +export interface ZoneContribution extends AppContributionBase { readonly kind: 'zone'; - /** Published zone id (see AppZone below). Unknown zone = generate-time error. */ - readonly zone: AppZone | (string & {}); // dashboard family widens with its own zones - /** SSR component; may import the plugin's islands for interactivity. */ - readonly module: ComponentRef; - /** Deterministic ordering among contributions in the same zone (then by plugin id, then id). */ + readonly zone: string; // validated against the HOST's published surface (below) + readonly module: ComponentRef; // SSR component; may import the plugin's islands readonly order?: number; } - -/** v1 app-surface zones — a published, versioned enum. Additive growth = minor; removal = v2. */ -export type AppZone = - | 'app.topbar.end' // right side of the scaffolded topbar (_layout.tsx) - | 'app.dashboard.panels' // the scaffolded /dashboard panel grid - | 'app.home.cards' // the scaffolded index page card strip - | 'app.footer'; // global footer strip - -/** Props every zone component receives from the host. Deliberately minimal at v1. */ -export interface ZoneProps { - readonly url: URL; - /** Host state slice defined by PluginHostState (04-host-runtime.md §5). */ - readonly host: PluginHostState; -} ``` -### 4. `NavContribution` — a typed nav entry +### 4. `nav` (S-10: discriminated target, no ambiguous strings) ```ts -export interface NavContribution extends FrontendContributionBase { +export interface NavContribution extends AppContributionBase { readonly kind: 'nav'; - readonly label: string; - /** Route contribution id (same plugin) or absolute href. */ - readonly route: string; - /** Host nav group. The scaffolded app has 'main' (topbar); hosts may define more. */ - readonly group?: string; + readonly label: MessageRef; + readonly target: + | { readonly kind: 'route'; readonly routeId: string } // same plugin; base-path composed + | { readonly kind: 'href'; readonly href: string } // host-internal absolute path + | { readonly kind: 'external'; readonly href: string }; // rel/noopener enforced by host + readonly group?: string; // validated against HostSurfaceDescriptor.navGroups readonly order?: number; } ``` -Nav entries compile to the existing fresh-ui nav data model — `SidebarNavItem` / -`SidebarNavSection` (`packages/fresh-ui/registry/components/ui/sidebar-shell.tsx:8-32`) and the -topbar in the scaffolded `_layout.tsx.template` — and to typed route references -(`createRouteReference`, `@netscript/fresh/route`) so `routes.plugins.crons.calendar.href()` is -available app-wide. - -### 5. `ThemeContribution` — token-safe styling +### 5. `theme` ```ts -export interface ThemeContribution extends FrontendContributionBase { +export interface ThemeContribution extends AppContributionBase { readonly kind: 'theme'; - /** CSS files consuming ONLY the --ns-* semantic vocabulary (fresh-ui tokens). */ - readonly css: readonly ComponentRef[]; + readonly css: readonly ComponentRef[]; // --ns-* vocabulary only; scoping rules in 04 §8 } ``` -v1 is a CSS overlay layer aggregated by the registry (`03-discovery-and-registry.md §3`); plugin -CSS is scoped under `[data-ns-plugin='<id>']` (`04-host-runtime.md §6`). DTCG token-file merging -into `tokens:build` is an explicit **non-goal at v1** (owner fork F5, `../plan.md`). - -## The union and the registry shape - ```ts -export type FrontendContribution = +export type ComponentRef = `./${string}`; +export type AppContribution = | RouteContribution | IslandContribution | ZoneContribution | NavContribution | ThemeContribution; +``` + +## Host surface negotiation (S-10) + +Zones and nav groups are **host data, not schema constants**. Each host publishes a versioned +descriptor; the registry validates contributions against the *host it is generated for*: + +```ts +export interface HostSurfaceDescriptor { + readonly host: string; // 'app' (scaffolded app), 'dashboard', … + readonly families: readonly FamilyRef[]; // supported (family, major) windows + readonly zones: readonly { readonly id: string; readonly capacity?: number }[]; + readonly navGroups: readonly string[]; + readonly reservedPaths: readonly string[]; // '/_fresh', '/api/plugins', host basePath rules +} +``` + +The scaffolded app template ships its descriptor (v1 zones: `app.topbar.end`, +`app.dashboard.panels`, `app.home.cards`, `app.footer`; nav group `main`). Adding a zone to a +host is a data change, not a contract change — which is what makes zone growth genuinely +additive (this, not schema openness, resolves rev-1's `(string & {})` hole). Diagnoses are +distinct: **unknown zone** (typo — generate-time error) vs **known-but-unmounted** (plugin fine, +surface absent — informational, not quarantine) vs **capacity-rejected** (deterministic overflow +report). -/** What .netscript/generated/frontend.registry.ts exports. */ -export interface FrontendContributionRegistry { - readonly contract: 'v1'; - readonly plugins: ReadonlyMap<string, ResolvedFrontendManifest>; +## Host context — split server/client (S-9) + +```ts +/** SERVER-ONLY — lives under Fresh app state (state.pluginHost); may hold functions/ports. */ +export interface PluginRequestContext { + readonly identity: FrontendIdentity; + readonly base: string; // resolved mount base + readonly serviceUrl: (service: string) => string; // host runtime-config/Aspire resolution + /** Principal PORT — supplied by the auth plugin when installed; base contract owns only the port shape. */ + readonly principal: PrincipalPort | null; + readonly csp: { readonly nonce?: string }; // Fresh CSP seam passthrough + readonly signal: AbortSignal; } -/** Manifest after registry resolution: refs expanded to absolute specifiers, base finalized. */ -export interface ResolvedFrontendManifest extends FrontendManifest { - readonly resolvedBase: string; // after host remap + collision check - readonly specifierOf: (ref: ComponentRef) => string; // '@acme/plugin-crons/frontend/...' +/** SERIALIZABLE — safe to pass to islands; versioned with the family schema. */ +export interface PluginClientContext { + readonly mountId: string; + readonly base: string; + readonly locale: string; + readonly direction: 'ltr' | 'rtl'; + readonly timeZone: string; + readonly subject: { readonly signedIn: boolean; readonly display?: string } | null; + readonly capabilities: readonly string[]; // granted requires, for UI degradation } ``` -## The pointer — the ONLY thing core learns +`SessionClaims` (a concrete auth type) appears in **neither** — the auth family supplies a +`PrincipalPort` implementation; unauthenticated hosts carry `principal: null` and no auth +dependency. Zone components receive `{ url, host: PluginRequestContext, client: +PluginClientContext }` server-side and may forward only `client` (or slices of it) to islands. + +## `requires` — the gateway's input, not prose (S-6 linkage) ```ts -// packages/plugin (additive; no vocabulary import) -export interface FrontendContributionRef { - readonly export: string; // './frontend' — a deno.json export of the plugin package - readonly framework: 'fresh'; // doctrine 07 axis discriminator; reserved for expansion - readonly contract: 'v1'; +export interface FrontendRequires { + readonly ports?: readonly string[]; + /** + * Procedure grants consumed by the generated deny-by-default gateway (04 §4): each entry + * names a procedure the plugin's OWN contract exposes; the gateway derives service owner, + * method, path template, and streaming policy from the plugin's versioned contract metadata. + * Not an audit comment — the actual allowlist. + */ + readonly procedures?: readonly string[]; } -// PluginContributions gains: readonly frontend?: FrontendContributionRef; -// PluginBuilder gains: .withFrontend(ref: FrontendContributionRef) -// PluginInstallerManifest (scaffold.plugin.json) gains the same block, parse-only: -// "frontend": { "export": "./frontend", "framework": "fresh", "contract": "v1" } ``` -The pointer mirrors `scaffolder.export` (`packages/plugin/src/protocol/manifest.ts:28-34`): -readable at install/doctor time without executing plugin code. Bump -`PLUGIN_MANIFEST_SCHEMA_VERSION` additively. - -## Validation & versioning - -- `defineFrontend()` (see `02-authoring-dx.md`) validates against the zod schemas and freezes — - same posture as `definePlugin().build()` - (`packages/plugin/src/config/builders/plugin-builder.ts:282`). -- Cross-checks at registry-generation time (not runtime): plugin id matches; route ids referenced - by nav exist; zone ids known for the targeted surfaces; base collisions; duplicate contribution - ids (doctrine 07: duplicate rejection with a structured error naming both contributors). -- **Versioning**: `contract: 'v1'` + JSR-versioned subpath `contracts/v1`. Additive evolution - (new zones, new optional fields, new kinds) stays v1; breaking cuts `contracts/v2` with a host - support window — carried unchanged from the ratified dashboard design - (`plugin-extension-architecture.md` §4), including the quarantine-not-crash render rule and the - three-fact drift surfacing (package / contract / peer). +## Validation & cross-checks + +- `defineFrontend()` validates the envelope + `app` payload schema and freezes (posture of + `definePlugin().build()`). +- Generate-time cross-checks (structured errors): pluginKind/package match; nav `routeId` + resolution; zone/navGroup validation against the target host descriptor; duplicate ids; base + collisions per the **full rules** in `03-discovery-and-registry.md §3` (route-pattern overlap, + reserved paths, host basePath composition — not string equality); module refs present in the + package export map. +- Quarantine (render-time) is reserved for contract-window mismatch and load failure — never for + known-but-unmounted surfaces. + +## The pointer — unchanged + +`@netscript/plugin` learns only `FrontendContributionRef = { export, framework: 'fresh', +contract: FamilyRef[] }` (builder `.withFrontend()`, installer-manifest `frontend` block, +parse-only). `PLUGIN_MANIFEST_SCHEMA_VERSION` bumps additively; older CLIs ignore the block — +and because the older *host* also lacks the frontend generate step, ignoring is safe (no +half-wired state). diff --git a/.llm/runs/plan-frontend-contrib--seed/design/canonical/02-authoring-dx.md b/.llm/runs/plan-frontend-contrib--seed/design/canonical/02-authoring-dx.md index c0b9fbdda..c8584c97a 100644 --- a/.llm/runs/plan-frontend-contrib--seed/design/canonical/02-authoring-dx.md +++ b/.llm/runs/plan-frontend-contrib--seed/design/canonical/02-authoring-dx.md @@ -1,7 +1,10 @@ -# Authoring DX — what a plugin author actually writes (draft) +# Authoring DX — what a plugin author actually writes (draft, rev 2) -> **Draft — design document only.** This document is the DX bar the implementation is held to. -> If a mechanism choice would make any snippet below longer or stranger, the mechanism is wrong. +> **Draft — design document only.** Rev 2 integrates adversarial findings S-5, S-16 (and the +> rev-2 contract shapes from S-7/S-8/S-10). This document remains the DX bar the implementation +> is held to: if a mechanism choice makes a snippet below longer or stranger, the mechanism is +> wrong — but the bar now also refuses *hidden* work (S-16): every step the author must do is on +> this page, none deferred to folklore. ## The pitch, in one screen @@ -12,14 +15,9 @@ A plugin frontend is **that, in a `frontend/` folder**, plus one declaration fil plugins/crons/ frontend/ mod.ts - routes/ - calendar.tsx - schedules/[id].tsx - _layout.tsx - islands/ - CronCalendar.tsx - components/ - NextFiresCard.tsx + routes/calendar.tsx routes/schedules/[id].tsx + islands/CronCalendar.tsx + components/NextFiresCard.tsx theme.css ``` @@ -28,8 +26,8 @@ plugins/crons/ import { defineFrontend } from '@netscript/plugin-frontend-core'; export default defineFrontend({ - contract: 'v1', - plugin: 'crons', + contract: { family: 'app', major: 1 }, + pluginKind: 'crons', base: '/crons', routes: [ { @@ -37,7 +35,7 @@ export default defineFrontend({ id: 'calendar', path: '/calendar', module: './routes/calendar.tsx', - nav: { label: 'Cron calendar', icon: 'calendar', group: 'main' }, + nav: { label: { id: 'crons.nav.calendar', default: 'Cron calendar' }, icon: 'calendar', group: 'main' }, }, { kind: 'route', id: 'schedule-detail', path: '/schedules/:id', module: './routes/schedules/[id].tsx' }, ], @@ -50,111 +48,90 @@ export default defineFrontend({ }); ``` -Register the axis on the plugin (one builder call + one manifest block): +`defineFrontend` validates and freezes; the keyed form compiles to the versioned envelope +(`01-contracts.md`). Register the axis on the plugin (one builder call + one manifest block): ```ts -// plugins/crons/src/public/mod.ts — existing definePlugin chain -export const cronsPlugin = definePlugin('@acme/plugin-crons', VERSION) - .withType('utility') - .withService({ name: 'crons-api', entrypoint: './services/src/main.ts', port: 8093 }) - .withFrontend({ export: './frontend', framework: 'fresh', contract: 'v1' }) // ← new - .build(); +// plugins/crons/src/public/mod.ts +.withFrontend({ export: './frontend', framework: 'fresh', contract: [{ family: 'app', major: 1 }] }) ``` ```jsonc -// plugins/crons/scaffold.plugin.json — additive block, parse-only discovery -{ "frontend": { "export": "./frontend", "framework": "fresh", "contract": "v1" } } +// plugins/crons/scaffold.plugin.json +{ "frontend": { "export": "./frontend", "framework": "fresh", "contract": [{ "family": "app", "major": 1 }] } } ``` -That's the complete authoring surface. Everything else — registry, islands build wiring, nav, -route refs, CSS aggregation, API proxy — is generated or host-side. - -## Writing the pages: it is just Fresh +## Writing the pages: it is just Fresh (with one import that is honest about where it lives) ```tsx -// plugins/crons/frontend/routes/calendar.tsx — an ordinary Fresh route module -import { definePluginPage } from '@netscript/plugin-frontend-core'; +// plugins/crons/frontend/routes/calendar.tsx +import { definePluginPage } from '@netscript/fresh/plugins'; // runtime sugar lives with the runtime (S-5) import { CronCalendar } from '../islands/CronCalendar.tsx'; import { createCronsClient } from '@acme/plugin-crons/contracts/v1'; export default definePluginPage(async (ctx) => { - // Server-side: typed client against the plugin's own service (base URL from host config). + // ctx is Fresh PageProps + typed ctx.host (PluginRequestContext, injected via app state). const crons = await createCronsClient(ctx.host.serviceUrl('crons-api')).crons.list(); return ( <section class='ns-stack ns-stack--lg'> <h1 class='ns-heading'>Cron calendar</h1> - <CronCalendar initial={crons} /> + <CronCalendar initial={crons} client={ctx.client} /> </section> ); }); +// Sugar-free equivalent: plain `define.page`, reading props.state.pluginHost yourself. ``` ```tsx -// plugins/crons/frontend/islands/CronCalendar.tsx — an ordinary Fresh island +// plugins/crons/frontend/islands/CronCalendar.tsx import { useSignal } from '@preact/signals'; -import { createCronsClient } from '@acme/plugin-crons/contracts/v1'; -import { pluginApi } from '@netscript/plugin-frontend-core/contracts/v1'; +import { pluginApi } from '@netscript/fresh/plugins'; +import type { PluginClientContext } from '@netscript/plugin-frontend-core/contracts/v1'; -export function CronCalendar(props: { initial: readonly CronEntry[] }) { +export function CronCalendar(props: { initial: readonly CronEntry[]; client: PluginClientContext }) { const entries = useSignal(props.initial); async function refresh() { - // Client-side: same typed client, same-origin proxy (/api/plugins/crons/…). - entries.value = await createCronsClient(pluginApi('crons')).crons.list(); + // Client-side calls go through the generated deny-by-default gateway (04 §4): + entries.value = await createCronsClient(pluginApi(props.client)).crons.list(); } - return ( /* fresh-ui-token-styled markup, --ns-* vars only */ ); + return ( /* --ns-* styled markup */ ); } ``` -Key DX properties, each load-bearing: - -- **Islands are imported and rendered directly** — the registry feeds their specifiers to the - build (`islandSpecifiers`), so hydration Just Works. No island wrapper API, no registration - call in author code. -- **Types flow end-to-end** — pages import the plugin's own `contracts/v1` client; the island - props are ordinary TypeScript. Breaking a contract breaks `deno check` in the generated - workspace at install time, not the user's browser at runtime. -- **`definePluginPage`** is thin sugar over Fresh's `define.page` that types `ctx.host` - (`PluginHostState`: service URL resolution, session claims, base path) — the ONLY new concept a - page author meets. Plain `define.page` route modules also work; the sugar is optional. -- **Styling uses the app's tokens.** Only `--ns-*` semantic vars - (`packages/fresh-ui/tokens/semantic.tokens.json`) — the plugin UI is automatically correct in - light/dark and under any app theme override. Compiled fresh-ui *components* are not importable - (copy-registry, app-owned); plugin markup uses tokens + its own CSS + the fresh-ui runtime - helpers that ARE package exports (`Icon`, `DataGrid`, toast, `cn`, interactive namespaces). - -## The dev loop +**The serialization rule (S-16, stated where authors will read it):** island props cross the +server→client boundary and must be serializable data — no functions, no `Request`, no server +context. Pass `ctx.client` (the serializable `PluginClientContext`), never `ctx.host`. The +`./testing` kit fails a contribution whose island props don't round-trip. + +## The dev loop — two modes, stated honestly (S-16) ``` -netscript plugin new crons --with frontend # scaffolds the frontend/ skeleton above -cd plugins/crons && deno task dev # plugin dev = the HOST app runs with local-source - # plugin; vite watches plugins/** (watchPaths seam - # already exists in createNetScriptVitePlugin) +netscript plugin new crons --with frontend # scaffolds the tree above + manifest + exports +netscript plugin dev # in the plugin dir: watches frontend/, maintains + # deno.json exports, regenerates the host registry + # atomically, signals vite (reload islands / HMR) ``` -- Local-source plugins are workspace members; editing `frontend/routes/calendar.tsx` hot-reloads - the host app like any app file (vite `watchPaths` already includes workspace packages — - `packages/fresh/src/application/vite/README.md`). -- `netscript generate plugins` re-emits the registry; `plugin install`/`uninstall` runs it - automatically. Regeneration is idempotent (byte-identical skip — the `runtime-schemas` - precedent). -- `netscript plugin doctor` gains a `frontend` check: contract handshake, zone validity, module - refs resolvable, deno.json exports present. - -## Convention generator (phase 2 sugar — the contract stays explicit) - -`netscript generate frontend` (run inside the plugin) derives the `routes:`/`islands:` lists from -the file tree (Medusa's build-time convention, done the NetScript generated-file way) and -maintains the plugin's `deno.json` exports for `./frontend` and each island/route module — JSR -requires explicit exports, so the generator owns that bookkeeping, not the author. Authors who -prefer to write the manifest by hand simply do; the generated form is the same contract. - -## What an author CANNOT do (guardrails, stated as DX) - -- Import the app's copied `components/ui/*` — those are app-owned. Use tokens + package-safe - fresh-ui runtime exports. (The auth example shows the scaffolded-starter path when you *want* - the app to own and restyle a page — `../examples/auth.md`.) -- Claim routes outside the mount base — base remap/collisions are host decisions - (`04-host-runtime.md §2`). -- Mutate the registry at runtime — there is no API for it, by design. -- Ship raw hex/oklch colors — theme CSS is `--ns-*` vocabulary (lint-gated later; debt entry - until then). +- **Local-source mode** (plugin in the workspace): route/component edits hot-reload through + vite `watchPaths`; island-list changes and manifest edits are picked up by + `netscript plugin dev`'s regeneration + reload signal. This is the primary authoring loop. +- **JSR-installed mode**: packages are immutable; the loop is publish → `plugin update`. Island + build behavior in this mode is Wave-0-gated ([P3], `04 §3`). Docs never pretend the two loops + are the same. + +**Export-map maintenance is tooling-owned from phase 1** (S-16 — was phase 2): JSR resolves only +published entrypoints, so `./frontend` and every route/island module need explicit `deno.json` +exports; `plugin new --with frontend` seeds them and `plugin dev` / `generate frontend` keep +them in sync. Authors never hand-maintain the export list (hand-written manifests are still +fine — the generator only owns the export map + optional convention-derived lists). + +## What an author CANNOT do (guardrails as DX) + +- Pass non-serializable island props (checked by the test kit; explained above). +- Import the app's copied `components/ui/*` (app-owned); use tokens + fresh-ui runtime exports. +- Ship a plugin `_layout` in v1 (rejected at generate time — S-2; wrap pages in a shared + component instead). +- Rely on SSR error boundaries for a crashing zone render — fetch in the resolver, keep render + pure (the containment contract, `04 §6`). +- Claim routes outside the mount base; mutate the registry at runtime; ship raw hex colors + (`--ns-*` only). diff --git a/.llm/runs/plan-frontend-contrib--seed/design/canonical/03-discovery-and-registry.md b/.llm/runs/plan-frontend-contrib--seed/design/canonical/03-discovery-and-registry.md index 300f3c439..d19fc7f05 100644 --- a/.llm/runs/plan-frontend-contrib--seed/design/canonical/03-discovery-and-registry.md +++ b/.llm/runs/plan-frontend-contrib--seed/design/canonical/03-discovery-and-registry.md @@ -1,7 +1,7 @@ -# Discovery & Registry — manifest to generated code (draft) +# Discovery & Registry — manifest to generated code (draft, rev 2) -> **Draft — design document only.** Mechanism follows the existing axis pipeline exactly; -> file names and shapes are sketches pending the adversarial pass. +> **Draft — design document only.** Rev 2 integrates adversarial findings S-2, S-3, S-10, S-11, +> S-12 (`../../adversarial-sol.md`; dispositions in `../../adversarial-triage.md`). ## 1. Discovery chain (all additive to existing machinery) @@ -9,112 +9,91 @@ scaffold.plugin.json "frontend" block ← parse-only pointer (no plugin code executed) │ netscript plugin install / generate plugins ▼ -import plugin's './frontend' export ← FrontendManifest (validated data) +import plugin's './frontend' export ← FrontendManifestEnvelope(s) (validated data) ▼ -emit .netscript/generated/frontend.* ← typed registry + islands + route refs + css +STAGE .netscript/generated/frontend.* set ← full replace-set, staged out-of-place ▼ -deno check generated workspace ← EXISTING install gate; type-broken contribution - fails install with a real diagnostic +deno check staged set incl. frontend.check.ts ← imports EVERY referenced module (routes, + islands, css) — a bad deep export fails HERE, + not at first vite build (S-11) + ▼ +atomic swap into .netscript/generated/ ← or rollback; never a half-updated host ``` -1. **Manifest block** — `PluginInstallerManifestSchema` gains an optional `frontend` object - (`packages/plugin/src/protocol/manifest.ts:204-216`); `parsePluginManifest` stays - execution-free. `capabilities` gains nothing: the block's presence IS the capability. -2. **Runtime manifest** — `PluginContributions.frontend?: FrontendContributionRef` + - `PluginBuilder.withFrontend()` - (`packages/plugin/src/config/domain/plugin-contributions.ts:12-38`, - `.../builders/plugin-builder.ts`). `CONTRIBUTION_AXES` gains `'frontend'` - (`packages/plugin/src/domain/constants.ts:16-40`) — the previously inert `PluginType - 'frontend'` value finally has machinery behind it. -3. **Emission** — `netscript generate plugins` - (`packages/cli/src/public/features/generate/plugins/generate-plugin-registries-command.ts`) - gains a frontend emitter alongside the per-axis registry emitter - (`packages/plugin/src/sdk/discovery/registry-emitter.ts`). Same AUTO-GENERATED / DO NOT EDIT - marker, same idempotency-by-construction, plus the byte-identical write skip - (`generate-runtime-schemas.ts:107-135` precedent). - -## 2. Generated files (the complete set) +Manifest block, runtime-manifest pointer, and `CONTRIBUTION_AXES` addition are unchanged from +rev 1 (`packages/plugin/src/protocol/manifest.ts:204-216`, +`packages/plugin/src/config/domain/plugin-contributions.ts:12-38`). -```ts -// .netscript/generated/frontend.registry.ts — AUTO-GENERATED, DO NOT EDIT -import cronsFrontend from '@acme/plugin-crons/frontend'; -import authFrontend from '@netscript/plugin-auth/frontend'; -import { buildFrontendRegistry } from '@netscript/plugin-frontend-core/contracts/v1'; - -export const frontendRegistry = buildFrontendRegistry([ - { manifest: cronsFrontend, specifierBase: '@acme/plugin-crons/frontend' }, - { manifest: authFrontend, specifierBase: '@netscript/plugin-auth/frontend' }, -], { - // host overrides from netscript.config (base remaps, disabled contributions) - overrides: hostOverrides, -}); -``` +## 2. Generated files — the complete replace-set (S-11) -```ts -// .netscript/generated/frontend.islands.ts — consumed by vite.config.ts -export const pluginIslandSpecifiers: readonly string[] = [ - '@acme/plugin-crons/frontend/islands/CronCalendar', - '@netscript/plugin-auth/frontend/islands/SessionMenu', -]; -``` +Every generation emits **all** of these, deterministically, even when empty (an empty registry +is a file that exports empty structures — so removal can never leave stale imports): + +| File | Contents | +| --- | --- | +| `frontend.registry.ts` | typed registry: identities, resolved bases, contributions, **literal lazy route loaders** | +| `frontend.islands.ts` | `pluginIslandSpecifiers: readonly string[]` for the vite feed | +| `frontend.routes.ts` | `createRouteReference` entries per plugin route (typed hrefs) | +| `frontend.css` | host layer-order prelude (`@layer ns-app, ns-plugins;`) + plugin css imports | +| `frontend.gateway.ts` | deny-by-default gateway route table from `requires.procedures` × contract metadata (04 §4) | +| `frontend.check.ts` | static import module referencing every route/island/css module — the type-check gate's teeth | + +Route loaders are **literal** (S-2): ```ts -// .netscript/generated/frontend.routes.ts — typed route references (createRouteReference idiom, -// packages/cli/src/kernel/assets/app/router.ts.template precedent) -export const pluginRoutes = { - crons: { - calendar: createRouteReference('/crons/calendar', { id: 'plugins.crons.calendar', kind: 'page' }), - scheduleDetail: createRouteReference('/crons/schedules/:id', { id: 'plugins.crons.scheduleDetail', kind: 'page' }), +// frontend.registry.ts (excerpt) — AUTO-GENERATED, DO NOT EDIT +import { normalizeFreshRouteModule } from '@netscript/fresh/plugins'; +export const routes_crons = [ + { + id: 'calendar', + path: '/calendar', + load: () => import('@acme/plugin-crons/frontend/routes/calendar').then(normalizeFreshRouteModule), }, - auth: { /* … */ }, -} as const; +] as const; ``` -```css -/* .netscript/generated/frontend.css — aggregated theme overlays, imported by assets/design.css */ -@import '@acme/plugin-crons/frontend/theme.css' layer(ns-plugins); -``` - -(If dep-CSS imports prove brittle under vite, fallback: generate-time copy into -`.netscript/generated/frontend/<plugin>.css` — decision deferred to implementation, both -mechanically equivalent. The fresh-ui CSS aggregator precedent is -`packages/cli/src/kernel/application/ui/registry.ts:110`.) - -Route-ref merging into the app's `router.ts` is **import-based, not file-mutation**: the -scaffolded `router.ts` template adds `...pluginRoutes` under a `plugins` key -(`05-scaffolding-and-cli.md §2`). Existing apps adopt via an idempotent `netscript generate -frontend-wiring` (same doc). +CSS: dependency `@import` is the primary mechanism; the copy fallback must rewrite relative +`url()` assets (S-12) — copied CSS is a transformed artifact, not equivalent bytes. ## 3. Resolution rules (generate-time, structured errors) -Deterministic order everywhere: plugins sorted by id, contributions by (`order`, plugin id, id) — -doctrine 07's deterministic-load-order law. +Deterministic order everywhere (plugins by mountId, contributions by (`order`, mountId, id)). | Check | Failure mode | | --- | --- | -| plugin id in manifest ≠ owning plugin | error naming both | -| base collision (two plugins → same resolvedBase, after overrides) | error naming both + remap hint (`netscript.config` override) | -| nav.route references unknown route id | error | -| unknown zone id for a targeted surface | error listing published zones | -| duplicate contribution id within plugin | error | -| `contract` outside host window | **quarantine**: contribution excluded, registry carries a `quarantined` entry rendered as the fix-CLI panel state (prior design §4), never a host crash | -| module ref missing from plugin's deno.json exports | error + `netscript generate frontend` hint | - -## 4. Install / uninstall / update lifecycle - -- `netscript plugin install` — existing post-scaffold wiring - (`packages/cli/src/public/features/plugins/install/install-plugin.ts:145-192`) gains one step: - regenerate frontend emissions. Works identically for local-source and JSR modes (the registry - imports by package specifier either way; local-source resolves through the workspace). -- `netscript plugin uninstall` — regeneration drops the plugin's contributions (registry is - derived state). Scaffolded **starter** files remain, app-owned, per the copy-model contract. -- `netscript plugin update` — package version bump + regeneration; contract drift surfaces via - doctor + the quarantine state, remediation CLI printed. -- `netscript plugin doctor` — new `frontend` check via the existing `DoctorCheckSpec` seam - (`packages/plugin/src/adapter/contract.ts`): handshake, zones, refs, exports. +| pluginKind ≠ owning plugin / packageName mismatch | error naming both | +| **Base/route collisions — full rules (S-20)**: route-*pattern* overlap across plugins (not string equality), reserved host paths (`/_fresh`, `/api`, gateway prefix, host `config.basePath` composition), nested/dynamic precedence conflicts | error with the colliding patterns + remap hint | +| nav `target.routeId` unknown | error | +| zone id not in the target host's `HostSurfaceDescriptor` | **unknown zone** → error (typo) | +| zone known to the contract family but absent from this host | **known-but-unmounted** → info diagnostic; contribution skipped, NOT quarantined (S-10) | +| zone capacity exceeded | deterministic overflow report naming winners/losers | +| duplicate contribution id within (plugin, family) | error | +| `(family, major)` outside the host's declared window | **quarantine** entry (fix-CLI render state), never a host crash | +| module ref absent from the package export map | error + `plugin dev`/`generate frontend` hint | + +## 4. Install / update / remove lifecycle (S-11) + +- **`netscript plugin install`** — post-scaffold wiring gains: regenerate the full replace-set → + staged `deno check` (incl. `frontend.check.ts`) → atomic swap. A type-broken contribution + fails install with the real diagnostic. +- **`netscript plugin update`** — same regeneration; contract drift surfaces via doctor + the + quarantine state, remediation CLI printed. +- **`netscript plugin remove`** (the actual verb — + `packages/cli/src/public/features/plugins/remove/remove-plugin-command.ts:39-69`; rev 1 said + "uninstall") — regeneration emits the deterministic empty set for departed plugins, so + registry/css/island imports can never dangle. Scaffolded **starter** files are app-owned and + survive removal by design; each starter lands with a provenance header comment naming the + generating plugin+resource so `plugin doctor` can report orphaned starters without ever + deleting them. +- **`netscript plugin doctor`** — `frontend` check: envelope/window handshake, zone validity vs + the host descriptor, export-map presence, **orphan/stale generated-output detection** (a + registry entry with no installed plugin, a css import with no file, a starter whose plugin is + gone). +- Generation is idempotent (byte-identical skip) and **transactional**: stage → check → swap or + rollback; a failed generation leaves the previous set untouched. ## 5. Gates carried from the ratified dashboard design (unchanged) -Install-time type-check (registry regeneration + `deno check`), doctor re-validation, render-time -quarantine-not-crash: `dashboard-design--orchestrator/analysis/plugin-extension-architecture.md` -§4 items 2a–2c apply verbatim to this layer. +Install-time type-check, doctor re-validation, render-time quarantine-not-crash +(`dashboard-design--orchestrator/analysis/plugin-extension-architecture.md` §4, items 2a–2c) — +now with the check module giving the install gate real coverage of deep module edges (S-3/S-11). diff --git a/.llm/runs/plan-frontend-contrib--seed/design/canonical/04-host-runtime.md b/.llm/runs/plan-frontend-contrib--seed/design/canonical/04-host-runtime.md index 27667d7c9..aa5f5c44c 100644 --- a/.llm/runs/plan-frontend-contrib--seed/design/canonical/04-host-runtime.md +++ b/.llm/runs/plan-frontend-contrib--seed/design/canonical/04-host-runtime.md @@ -1,155 +1,156 @@ -# Host Runtime — mounting, SSR/hydration, data, nav, theming, isolation (draft) +# Host Runtime — mounting, SSR/hydration, data, nav, theming, isolation (draft, rev 2) -> **Draft — design document only.** All mount paths wrap verified upstream primitives -> (research.md §3.1); the new code is glue in `@netscript/fresh` (new `./plugins` subpath). +> **Draft — design document only.** Rev 2 integrates adversarial findings S-1, S-2, S-3, S-4, +> S-6, S-9, S-12 (`../../adversarial-sol.md`; dispositions in `../../adversarial-triage.md`). +> Mechanisms marked **[P#]** are Wave-0 proof gates (`../../plan.md`) — the design is not +> considered verified until the proof passes. ## 1. The host entry — one option on `defineFreshApp` ```ts -// app/main.ts (scaffold template, after) -import { defineFreshApp } from '@netscript/fresh/server'; -import { frontendRegistry } from './.netscript/generated/frontend.registry.ts'; - export const app = defineFreshApp<State>({ name: '{{appName}}', - frontend: frontendRegistry, // ← the entire host-side integration + frontend: frontendRegistry, }); ``` -`defineFreshApp` (`packages/fresh/src/runtime/server/define-fresh-app.ts`) already exposes -`middleware[]` / `preConfigure` / `configure` seams; the `frontend` option is sugar that runs the -mount sequence below inside `configure`. Apps that need control call the pieces directly from -`@netscript/fresh/plugins`: +**Composition-phase law (S-1).** Fresh compiles each route's middleware/layout chain at +insertion time — later `use()` calls are not retroactive, and host fs layouts cannot wrap routes +registered before them. `defineFreshApp` currently runs `configure()` *before* `fsRoutes` +(`packages/fresh/src/runtime/server/define-fresh-app.ts:110-127`), so plugin mounting must NOT +ride `configure`. The `frontend` option instead runs a dedicated **post-fsRoutes composition +phase**: staticFiles → host middleware → host fs routes/layouts → **`mountPluginFrontends`**. +Within each plugin sub-app the registration order is fixed: **middleware first, then routes** +(plugin `_layout` is not supported in v1 — S-2). Contributed sub-apps may not carry `App`-level +`notFound`/`onError`/`appWrapper` commands; the mount glue strips them and reports. The exact +interleaving (host `_app` wrap, nested host layouts, host `config.basePath`, plugin middleware, +plugin 404 behavior, two plugins mounted) is **[P1]** with integration fixtures — D6 stands only +if P1 passes; the fallback mount mechanism is explicit `app.route()` registration of +fully-prefixed paths without `mountApp`. + +## 2. Route mounting ```ts -import { mountPluginFrontends, pluginApiProxy, pluginNavSections } from '@netscript/fresh/plugins'; +// inside mountPluginFrontends (sketch, per plugin) +const sub = new App<State>(); +sub.use(pluginScopeMiddleware(identity)); // BEFORE routes — S-1 +for (const r of m.routes) sub.route(r.path, r.load); // r.load: generated literal lazy loader +app.mountApp(m.resolvedBase, sub); ``` -## 2. Route mounting — sub-app per plugin - -For each plugin with route contributions the glue builds an upstream sub-`App` and mounts it: +**Route loaders are generated literals (S-2).** Upstream `App.route()` accepts +`MaybeLazy<Route>` — an internal `{ component, handler, config, css }` shape, not a fs route +module; only `fsRoutes` normalizes module namespaces. The registry therefore emits, per route: ```ts -// inside mountPluginFrontends (sketch) -for (const [pluginId, m] of registry.plugins) { - const sub = new App<State>(); - if (m.layoutModule) sub.layout('*', lazy(m.specifierOf(m.layoutModule))); // plugin _layout - for (const r of m.routes ?? []) sub.route(r.path, lazy(m.specifierOf(r.module))); - sub.use(pluginScopeMiddleware(pluginId)); // sets data-ns-plugin ctx + PluginHostState - app.mountApp(m.resolvedBase, sub); // upstream App.mountApp (core src/app.ts:357) -} -``` - -- **Base policy (locked, owner fork F2 records the alternative):** the plugin declares a - preferred `base` (default `/<pluginId>`); the **host** may remap per plugin in - `netscript.config` (`frontend: { basePaths: { crons: '/tools/crons' } }`); collisions are - generate-time errors. The dev-dashboard host auto-remaps everything under - `/plugins/<pluginId>/…` — same registry, different policy. -- Plugin routes are lazy (`MaybeLazy<Route>` upstream) — code-splitting for free. -- A plugin `_layout` wraps only that plugin's pages; the app's root layout still applies - (upstream layout nesting semantics). - -## 3. Islands — build-time registration, both build paths - -- **Vite (scaffold default):** `vite.config.ts` template passes the generated list — - `fresh({ islandSpecifiers: [...pluginIslandSpecifiers] })` - (`@fresh/plugin-vite@1.0.8 src/mod.ts:56-63,211-214`; scaffold template currently calls bare - `fresh()` — `packages/cli/src/kernel/assets/app/vite.config.ts.template:44`). -- **Non-vite builder:** `withPluginIslands(builder, registry)` → - `builder.registerIsland(specifier)` (`@fresh/core@2.3.3 src/dev/builder.ts:157`). - -Because registration is by specifier, plugin code imports its islands like any component; SSR, -hydration, and props serialization are stock Fresh behavior. **Boundary rule (documented, not -new machinery):** island props must be serializable — the standard Fresh constraint. - -## 4. Data access — server direct, client via proxy - -- **Server (route handlers, zone components):** typed clients from the plugin's own - `contracts/v1` against the plugin service URL, resolved from host runtime config / Aspire - service discovery via `ctx.host.serviceUrl('<service-name>')`. Services are an existing axis - (`ServiceContribution` — `packages/plugin/src/config/domain/service-contribution.ts`). -- **Client (islands):** same typed clients pointed at a same-origin proxy: - `pluginApiProxy(registry)` middleware mounted at `/api/plugins/:pluginId/*` - (upstream `app.use(path, mw)`), forwarding to the plugin service base URL. Precedent: the AI - chat island already streams through an app-local proxy path - (`plugins/ai/.../chat-route.stub.ts` → `/api/ai/chat-stream`; - `packages/fresh/src/runtime/ai/stream-proxy.ts`). The proxy forwards session/auth headers - set by app middleware; `requires.procedures` is the audit surface for what flows (enforcement - = dashboard-run scope). -- SSE/streaming passes through (the stream-proxy precedent covers eventIterator contracts). - -## 5. Host state — `PluginHostState` (the SSR/hydration contract seam) - -```ts -// contracts/v1 (sketch) — what plugin server code may assume about the host -export interface PluginHostState { - readonly plugin: string; // the mounted plugin id - readonly base: string; // resolvedBase, for building intra-plugin hrefs - readonly serviceUrl: (service: string) => string; - /** Session claims when the auth plugin is installed; null otherwise. Shape owned by plugin-auth-core. */ - readonly session: SessionClaims | null; -} -``` - -Populated by `pluginScopeMiddleware` from app state. Plugin pages must not reach past this seam -into app-specific `State` — the seam is what keeps contributions portable across hosts (app, -dashboard). `definePluginPage` types `ctx.host` so the constraint is ergonomic, not policed. - -## 6. Zones — SSR injection with provenance - -```tsx -// app markup (scaffold templates place these; apps may add/remove freely) -import { PluginZone } from '@netscript/fresh/plugins'; -<PluginZone id='app.dashboard.panels' /> -``` - -`PluginZone` renders every `ZoneContribution` targeting that zone (deterministic order), each -wrapped: - -```html -<div data-ns-plugin='crons' data-ns-contribution='crons/next-fires'> …component… </div> +() => import('@acme/plugin-crons/frontend/routes/calendar').then(normalizeFreshRouteModule) ``` -- SSR-only by default; interactivity comes from islands the zone component imports. -- A crashed zone component renders the quarantine card (fix CLI + provenance), never breaks the - page — the prior design's render-time rule applied at the component boundary - (Preact error boundary in `PluginZone`). -- Zone occupancy is inspectable: dev-mode overlay listing zones + occupants is a dashboard-run - deliverable; the data (registry) already supports it. - -## 7. Nav — feeding the existing shell, typed - -- `pluginNavSections(registry, { group })` → `readonly SidebarNavSection[]` — directly - spreadable into `SidebarShell.navigation` - (`packages/fresh-ui/registry/components/ui/sidebar-shell.tsx:24-36`) and the scaffolded topbar - (`_layout.tsx.template:33-74`), with `matchPrefix` set from the plugin base. -- Typed hrefs everywhere: `routes.plugins.crons.calendar.href()` via the generated - `frontend.routes.ts` merged in `router.ts` — no string paths in app code. - -## 8. Theming & style isolation - -- Plugin CSS is layered (`@layer ns-plugins`) below app CSS — apps always win specificity wars — - and scoped under `[data-ns-plugin='<id>']` (the wrapper from §6 and the plugin layout from §2). -- Only `--ns-*` semantic vars (`packages/fresh-ui/tokens/semantic.tokens.json`, - `registry/theme/theme-bridge.css`): plugin UI inherits light/dark and any app theme override - with zero plugin-side work. `data-theme` switching (scaffold `_app.tsx` precedent) is free. -- Tailwind utilities inside plugin modules: plugin files are NOT in the app's Tailwind content - scan; v1 rule is **plain CSS + tokens (+ inline `class` on fresh-ui runtime components)** for - plugin-served UI. (Extending the app's Tailwind `content` globs to plugin packages is a - possible phase-2 nicety — flagged, not promised.) - -## 9. Isolation & failure containment (app surface = T0) - -- Installed plugins already run arbitrary server code — the app surface trusts them (research.md - §6.5). Containment here is about *blast radius of bugs*, not malice: lazy route mounting - (a broken page module fails on navigation, not boot), zone error boundaries, quarantined - contract drift, CSS layering. -- The `requires` declaration + provenance wrappers are the audit surface the dashboard's T1/T2 - tiers will later enforce (iframe + RPC bridge — dashboard run, [stable]). - -## 10. What is explicitly NOT built - -No runtime registration API; no plugin-to-plugin frontend imports (composition happens through -zones/nav, or through explicitly published packages); no client-side plugin router (Fresh -partials/`f-client-nav` already cover SPA-feel navigation); no shadow-DOM/web-component wrapper -(tokens + layers + scoping attributes suffice at T0). +- literal specifier → real bundler edge (no computed `import()`); +- `normalizeFreshRouteModule` (owned by `@netscript/fresh/plugins`, **[P2]**) maps + `default` / `handler`/`handlers` / `config` / `css` onto the pinned `Route` shape and throws a + structured error on unknown members. + +Base policy unchanged from rev 1 (plugin-preferred `base`, host remap, dashboard auto-remaps +under `/plugins/<mountId>`), with collision rules hardened in `03 §3`. + +## 3. Islands — verified API, proof-gated behavior [P3] + +Registration is by module specifier on both build paths — `fresh({ islandSpecifiers })` +(plugin-vite 1.0.8 `mod.ts:56-63,211-214`) and `Builder.registerIsland` (core +`dev/builder.ts:157`). That much is verified. **Not yet verified (S-3):** clean-cache resolution +(plugin-vite's Deno plugin runs `cachedOnly: true`), the Preact JSX transform's +`node_modules`/npm-cache excludes vs JSR-served TSX, dependency CSS, HMR (the island watcher +only watches local island dirs), published-JSR vs local-source parity, and `^1.0.8` range drift. +[P3] runs the full matrix (clean env × {local-source, jsr} × {dev, prod build + SSR + hydrate} + +single Preact identity + dependency CSS + edit-loop behavior) before the island contract is +frozen; until then authors' islands are supported for local-source plugins and the JSR mode is +gated on the proof. Known consequence either way: island edits in dependency mode may require a +signaled full reload rather than HMR — `netscript plugin dev` (05) owns that signal. + +## 4. Data access — a generated gateway, not a proxy (S-6) + +The rev-1 wildcard `/api/plugins/<id>/*` forwarding proxy is withdrawn — a header-forwarding +generic proxy is a confused-deputy/request-smuggling surface, and the existing AI stream proxy +(`packages/fresh/src/runtime/ai/stream-proxy.ts:55-211`) shows what a safe transport actually +handles (server-side auth, constrained upstream, manual redirects, rebuilt header set, abort). + +Replacement: **`pluginGateway(registry)`** — a generated, deny-by-default route table: + +- One route per granted procedure, generated from `requires.procedures` × the plugin's own + versioned contract metadata (service owner, method, path template, request/response mode, + streaming policy). Nothing else is reachable — no wildcard forwarding, ever. +- Server-side authentication/authorization via the host principal port; browser credentials are + never blind-forwarded; CSRF/origin checks; request/response size limits; timeouts; abort + propagation; `redirect: 'manual'`; response-header allowlist; structured audit log line per + invocation (mountId, procedure, subject). +- Streaming: SSE/eventIterator procedures declare it in metadata; the gateway applies the + stream-proxy discipline. The AI durable-chat plane **stays on its specialized adapter** — the + gateway does not replace `@netscript/fresh/ai`. +- Threat model + abort/reconnect semantics are **[P5]**, and the gateway is its own reviewed + implementation wave (`plan.md`). + +Server-side plugin code doesn't need the gateway: route handlers/zone components call typed +clients against `ctx.host.serviceUrl(...)` directly (unchanged). + +## 5. Host context — the split seam (S-9) + +`state.pluginHost: PluginRequestContext` (server-only: identity, base, serviceUrl, principal +port, CSP nonce seam, abort signal) is injected by `pluginScopeMiddleware`; +`PluginClientContext` (serializable: mountId, base, locale, direction, timeZone, subject +summary, capabilities) is what may cross into islands. Shapes in `01-contracts.md`. Zone +components receive both; pages read them from state via the `definePluginPage` helper — which +lives in **`@netscript/fresh/plugins`** (S-5), typed over Fresh `PageProps` with the injected +state; plain `define.page` + `props.state.pluginHost` is the sugar-free equivalent. + +## 6. Zones — SSR injection with honest containment (S-4) + +`<PluginZone id>` renders zone contributions in deterministic order, each wrapped in +`<div data-ns-plugin='<mountId>' data-ns-contribution='<mountId>/<id>'>`. + +**Containment contract (downgraded from rev 1, which overclaimed):** Fresh renders with +`preact-render-to-string` without its error-boundary mode, and async components run before the +boundary exists — so a *render-time throw in SSR is not containable by a component boundary*. +What the host actually guarantees: + +1. **Data-phase containment**: zone data resolution runs host-side *before* render inside a + try/catch; a failed resolution renders the quarantine card. Zone components are told to keep + render pure — fetch in the resolver, not the component body. +2. **Client containment**: hydrated islands sit under a client-side Preact boundary; post- + hydration crashes degrade to the quarantine card without killing the page. +3. **Route-level `onError`**: a plugin route that throws fails that route, host-styled — never + the shell. +4. An SSR render-time throw in a zone component **fails the page response** — documented, tested + ([P4] fixture), and the reason the resolver-not-render rule exists. If hard SSR zone isolation + is ever required, it is a designed isolated-render protocol (own wave), not a boundary claim. + +## 7. Nav — unchanged mechanics, validated data + +`pluginNavSections(registry, { group })` feeds `SidebarShell.navigation` / the topbar; targets +are discriminated (`route`/`href`/`external` — S-10) with base-path composition and +`rel="noopener noreferrer"` on external; labels are `MessageRef`s resolved through the host's +message resolution (default text when no catalog). Typed route refs unchanged +(`frontend.routes.ts`). + +## 8. Theming & style isolation (S-12) + +- **Layer order is host-owned**: the generated CSS begins with a host prelude declaring + `@layer ns-app, ns-plugins;` (order, not names, decides priority) so plugin CSS loses to app + CSS by declaration, not by luck. +- Scoping: `[data-ns-plugin='<mountId>']` wrappers (zones, plugin route trees). **Portals + escape wrappers**: the host provides a per-plugin portal root (`<div data-ns-plugin=…>` under + `document.body`) via the client context, and fresh-ui overlay primitives mount there; + arbitrary portals outside it are out of contract. +- Copy-fallback caveat: copying dependency CSS into `.netscript/generated/` changes the base URL + for relative `url()` assets — the generator must rewrite `url()`s or inline; recorded as an + implementation requirement, with a full `AssetContribution` (hashing, cache headers) + registered as a debt candidate (06 §4), not silently equivalent bytes. +- Tokens rule unchanged: `--ns-*` vocabulary only; light/dark/theme override inheritance for + free. + +## 9. Isolation posture (unchanged) and 10. Non-goals (unchanged) + +App surface = T0 trusted (installed plugins already run server code); the `requires` block + +provenance wrappers are the audit surface; T1/T2 iframe tiers remain dashboard-run scope. No +runtime registration API; no plugin-to-plugin imports outside published exports; no client-side +plugin router; no shadow-DOM wrapper. diff --git a/.llm/runs/plan-frontend-contrib--seed/design/canonical/05-scaffolding-and-cli.md b/.llm/runs/plan-frontend-contrib--seed/design/canonical/05-scaffolding-and-cli.md index 5e41f5b6c..6a5e88fd3 100644 --- a/.llm/runs/plan-frontend-contrib--seed/design/canonical/05-scaffolding-and-cli.md +++ b/.llm/runs/plan-frontend-contrib--seed/design/canonical/05-scaffolding-and-cli.md @@ -11,8 +11,9 @@ | `netscript plugin install <spec>` | unchanged UX; now also regenerates frontend emissions | existing post-install wiring (`install-plugin.ts:145-192`) | | `netscript generate plugins` | now also emits `frontend.*` | per-axis emitter (`registry-emitter.ts`) | | `netscript generate frontend-wiring` | idempotent adoption for EXISTING apps: vite `islandSpecifiers` feed, `router.ts` plugin-routes merge, `<PluginZone>` hints, css import | `ui:init` + `generate runtime-schemas` idempotency | -| `netscript plugin new <name> --with frontend` | plugin-side skeleton (`frontend/` tree + manifest block + exports) | `generate-plugin-mod.ts` template | -| `netscript generate frontend` (in plugin dir) | phase-2 convention generator: file tree → manifest lists + deno.json exports maintenance | `tokens:build` / assets-barrel generated-file idiom | +| `netscript plugin new <name> --with frontend` | plugin-side skeleton (`frontend/` tree + manifest block + **seeded deno.json exports**) | `generate-plugin-mod.ts` template | +| `netscript plugin dev` (in plugin dir) | **phase-1** (S-16): watches `frontend/`, maintains the export map, regenerates the host registry atomically, signals vite reload when island inputs change | `watch-run.ts` wake idiom | +| `netscript generate frontend` (in plugin dir) | phase-2 convention generator: file tree → manifest contribution lists (export-map maintenance is already phase-1 via `plugin dev`) | `tokens:build` / assets-barrel generated-file idiom | | `netscript plugin resource add <plugin> <resource> --app <path>` | scaffolded-starter (copy-mode) resources into a chosen app | DDX-19 #432 one-generator-two-callers; `chatRouteScaffolder` | | `netscript plugin doctor` | + `frontend` check | `DoctorCheckSpec` seam | | `netscript ui *` | unchanged — fresh-ui copy-registry stays the app-owned component channel | `application/ui/registry.ts` | @@ -73,10 +74,19 @@ my-app/ netscript.config.* ← host overrides: basePaths, disabled contributions ``` -## 5. E2E surface (gate wiring, for the plan) +## 5. Quality surface (gate wiring, for the plan — S-18) -`scaffold.runtime` suite extension (design-time note, not run by this seed): install a -frontend-contributing plugin → assert generated emissions exist and type-check → boot app → -assert contributed route serves, nav entry present, zone renders, island hydrates (Playwright, -SCOPE-frontend gates), uninstall → assert clean disappearance. This is the acceptance shape for -phase 1 (`../plan.md` §Gates). +Two layers, both specified in `../plan.md` §Gates: + +- **Per-plugin test kit** (`@netscript/plugin-frontend-core/testing`): a generated host fixture + that runs schema/resolution checks, island-props serialization round-trip, SSR render, + hydration, browser smoke (Playwright), a11y (axe + keyboard), base-path composition, and + local-source + JSR modes. A plugin frontend ships with this suite green — the analog of the + e2e axis plugins already declare. +- **Budgets**: per-plugin production budgets recorded in the manifest and asserted by the kit — + initial JS bytes, async chunk count, CSS bytes, island count, zone SSR render deadline, data + resolver deadline. Exceptions are explicit host policy, not silent convention. +- **`scaffold.runtime` extension** (merge-readiness only): install a frontend-contributing + plugin → generated set exists + type-checks → boot → contributed route serves, nav present, + zone renders, island hydrates → `plugin remove` → clean disappearance (empty emissions, no + dangling imports). diff --git a/.llm/runs/plan-frontend-contrib--seed/design/canonical/06-doctrine-fit.md b/.llm/runs/plan-frontend-contrib--seed/design/canonical/06-doctrine-fit.md index 744afc4c3..121d882bb 100644 --- a/.llm/runs/plan-frontend-contrib--seed/design/canonical/06-doctrine-fit.md +++ b/.llm/runs/plan-frontend-contrib--seed/design/canonical/06-doctrine-fit.md @@ -52,8 +52,11 @@ SCOPE-frontend browser/route/state gates for everything user-visible. | `--ns-*`-only CSS lint for plugin theme files | needs a lint rule; v1 is documentation + review | | Tailwind content-scan extension to plugin packages | vite/tailwind config reach; plain CSS suffices v1 | | DTCG token-file merge from plugins into `tokens:build` | owner fork F5 deferred | -| Island props serializability static check | upstream Fresh constraint; no static gate exists | +| Island props serializability *static* check | runtime round-trip check ships in the test kit (S-16); a static gate needs tooling | | Zone occupancy caps / conflicts UI | dashboard-run inspector covers observability first | +| `AssetContribution` (hashed assets, cache headers, integrity) | S-12: v1 covers css with url() rewrite rules; full asset contract is its own slice | +| SSR zone isolation protocol | S-4: containment contract is data-phase + client-side in v1; hard SSR isolation only as a designed protocol if ever demanded | +| Plugin message catalogs (full i18n pipeline) | S-17: v1 ships MessageRef + locale/direction/timeZone seams; catalog loading/fallback is an implementation wave | ## 5. Precedence honored (what this design reuses rather than invents) diff --git a/.llm/runs/plan-frontend-contrib--seed/design/examples/ai.md b/.llm/runs/plan-frontend-contrib--seed/design/examples/ai.md index 2012e07ba..dbafda6f6 100644 --- a/.llm/runs/plan-frontend-contrib--seed/design/examples/ai.md +++ b/.llm/runs/plan-frontend-contrib--seed/design/examples/ai.md @@ -1,7 +1,14 @@ -# Worked example — AI surfaces (draft) +# Worked example — AI surfaces (draft, rev 2) -> **Draft — design document only.** Upgrades the one existing plugin→app frontend precedent -> (scaffold-only chat route) into the two-model story, without new chat machinery. +> **Draft — design document only.** Rev 2 integrates adversarial finding S-14: rev 1 pointed the +> durable-chat runtime at a generic proxy path it cannot use — `createNetScriptChatConnection` +> requires a durable-session target (`packages/fresh/src/runtime/ai/create-chat-connection.ts:378-424`) +> and its transport is the specialized stream proxy, not the gateway. **v1 architecture (chosen): +> the durable-session runtime.** The plugin's live chat route ships WITH its specialized proxy +> route (a generated route contribution wired to `@netscript/fresh/ai`'s stream-proxy handler + +> the host principal port for authorization). The gateway (`04 §4`) serves the plugin's ordinary +> oRPC procedures (`ai.models`); it does not carry the chat stream. The oRPC event-iterator +> client is the documented alternative for hosts that opt out of durable sessions. ## Today @@ -50,8 +57,10 @@ export default defineFrontend({ ``` The live `Chat.tsx` island is the productionized `chat-route.stub.ts` content moved INTO the -plugin: `createNetScriptChatConnection` against the plugin API proxy -(`/api/plugins/ai/chat` — SSE passes through, `04-host-runtime.md §4`), fresh-ui token styling. +plugin: `createNetScriptChatConnection` with a **durable-session target** served by the plugin's +own contributed stream-proxy route (the `@netscript/fresh/ai` handler + host principal +authorization — S-14), fresh-ui token styling. Session identity, reconnect, and cancel semantics +are the durable-stream plane's, already shipped; the contribution layer only mounts it. ## The starter stays — repositioned, not removed @@ -67,7 +76,7 @@ eject path is honest: the starter is the same component source, scaffolded once, | Axis-5 need | Where it lands | | --- | --- | | Reusable assist affordances in arbitrary app surfaces | `AssistLauncher` island — any app page or plugin zone component can import & render it (islands are package exports) | -| Contextual "explain this / fix this" on other plugins' surfaces | those plugins import `@netscript/plugin-ai/frontend/islands/AssistLauncher` and pass context — plugin-to-plugin composition through **published exports**, the sanctioned path (`04-host-runtime.md §10`) | +| Contextual "explain this / fix this" on other plugins' surfaces | those plugins declare a **capability requirement** on the ai plugin (`requires.ports: ['ai-assist']`) and render the launcher only when granted (`ctx.client.capabilities`) — the import of `AssistLauncher` is the published-export composition path, gated by the capability so a host without ai degrades cleanly (S-14) | | Procedures-as-agent-tools (`DashboardAiToolContribution`) | **dashboard family extension**, not this base layer — the dashboard host's tool registry consumes it (`../canonical/00-overview.md` out-of-scope table) | ## After `netscript plugin install @netscript/plugin-ai` diff --git a/.llm/runs/plan-frontend-contrib--seed/design/examples/auth.md b/.llm/runs/plan-frontend-contrib--seed/design/examples/auth.md index 7f604f52a..1b5fd39f3 100644 --- a/.llm/runs/plan-frontend-contrib--seed/design/examples/auth.md +++ b/.llm/runs/plan-frontend-contrib--seed/design/examples/auth.md @@ -1,102 +1,88 @@ -# Worked example — Auth UI (draft) +# Worked example — Auth UI (draft, rev 2) -> **Draft — design document only.** Demonstrates BOTH delivery models on one plugin. +> **Draft — design document only.** Rev 2 integrates adversarial finding S-13: rev 1 designed an +> org/member console against `auth.org.*` procedures that **do not exist** in the v1 auth +> contract. The v1 example now uses only the real backend surface; org management is explicitly +> a future capability with a named backend prerequisite. ## Today -Mature backend, zero frontend: oRPC `signin/callback/signout/session/me` -(`packages/plugin-auth-core/src/contracts/v1/auth.contract.ts:437-459`); better-auth adapter with -`organization/admin` plugins (`packages/auth-better-auth/src/better-auth.ts:56,132-138`); workos -(`organizationId`) and kv-oauth adapters. No `.tsx` in any auth package. +Mature backend, zero frontend. The **complete** v1 auth contract is five procedures: +`signin`, `callback`, `signout`, `session`, `me` +(`packages/plugin-auth-core/src/contracts/v1/auth.contract.ts:410-459`). Session claims are a +generic claims record (`:74-88`) — no typed `orgId`. Adapters: better-auth (org/admin as +*backend* better-auth plugins — no NetScript contract surface yet), workos, kv-oauth. No `.tsx` +anywhere. -## Contribution declaration +## Contribution declaration (v1 — real surface only) ```ts // plugins/auth/frontend/mod.ts import { defineFrontend } from '@netscript/plugin-frontend-core'; export default defineFrontend({ - contract: 'v1', - plugin: 'auth', + contract: { family: 'app', major: 1 }, + pluginKind: 'auth', base: '/auth', routes: [ - // LIVE: the plugin owns account/org management — ships and evolves with the package. + // LIVE: the signed-in account page — session + profile + sign-out. Driven entirely by + // session/me/signout. { kind: 'route', id: 'account', path: '/account', module: './routes/account.tsx' }, - { - kind: 'route', - id: 'org', - path: '/org', - module: './routes/org.tsx', - nav: { label: 'Organization', icon: 'users', group: 'main' }, - }, - { kind: 'route', id: 'org-members', path: '/org/members', module: './routes/org/members.tsx' }, - ], - islands: [ - { kind: 'island', id: 'session-menu', module: './islands/SessionMenu.tsx' }, - { kind: 'island', id: 'member-table', module: './islands/MemberTable.tsx' }, ], + islands: [{ kind: 'island', id: 'session-menu', module: './islands/SessionMenu.tsx' }], zones: [ - // Session widget appears in the app topbar the moment auth is installed. + // Session widget in the app topbar the moment auth is installed: SSRs signed-in/out state + // from the principal port; SessionMenu island provides the dropdown + sign-out action. { kind: 'zone', id: 'session', zone: 'app.topbar.end', module: './components/SessionWidget.tsx' }, ], - requires: { procedures: ['auth.session', 'auth.me', 'auth.signout', 'auth.org.*'] }, + requires: { procedures: ['auth.session', 'auth.me', 'auth.signout'] }, }); ``` -```ts -// plugins/auth/src/public/mod.ts — one added builder call -.withFrontend({ export: './frontend', framework: 'fresh', contract: 'v1' }) -``` - -## The sign-in page is a SCAFFOLDED STARTER, not a live route +## The sign-in page is a SCAFFOLDED STARTER (unchanged from rev 1) -Sign-in is the page every product restyles — brand, copy, layout. Serving it live would fight the -user; so auth ships it as a starter resource through the existing scaffolder engine +Sign-in is the page every product restyles; auth ships it as a starter resource (`05-scaffolding-and-cli.md §3`): ``` netscript plugin resource add auth signin --app . - → routes/auth/signin.tsx (app-owned from now on; posts to the auth contract) - → routes/auth/callback.tsx (provider redirect landing — kv-oauth handleCallback / better-auth) + → routes/auth/signin.tsx (app-owned; drives signin → provider redirect) + → routes/auth/callback.tsx (redirect landing → callback procedure) ``` -The generated `signin.tsx` is ordinary app code: fresh-ui-token styling, typed -`createAuthClient(...)` calls against `signin`/`callback` — the user edits it freely; `plugin -update` reports drift, never overwrites (`ui registry` posture). - -**Rule of thumb applied:** user-owned surface (sign-in) → scaffolded; plugin-owned surface -(org/member console, session widget) → live. +Both generated files call only the real contract (`signin`/`callback`); styling is app tokens; +`plugin update` reports drift, never overwrites. **Starter scope law (S-13):** starters compose +existing backend procedures — a starter never fabricates backend functionality. -## What a page looks like (live org console) +## What a live page looks like (account, v1) ```tsx -// plugins/auth/frontend/routes/org/members.tsx -import { definePluginPage } from '@netscript/plugin-frontend-core'; +// plugins/auth/frontend/routes/account.tsx +import { definePluginPage } from '@netscript/fresh/plugins'; import { createAuthClient } from '@netscript/plugin-auth/contracts/v1'; -import { MemberTable } from '../../islands/MemberTable.tsx'; export default definePluginPage(async (ctx) => { - if (!ctx.host.session) return ctx.redirect('/auth/signin'); // starter page, if scaffolded + if (!ctx.host.principal) return ctx.redirect('/auth/signin'); const client = createAuthClient(ctx.host.serviceUrl('auth-api')); - const members = await client.org.members({ org: ctx.host.session.orgId }); - return <MemberTable initial={members} />; // island refreshes via pluginApi('auth') proxy + const me = await client.me(); + return ( /* profile card + session facts + <SessionMenu client={ctx.client}/> */ ); }); ``` -`SessionWidget` (zone component) SSRs the signed-in state from `ctx.host.session` (populated by -the auth middleware the plugin's service wiring already provides) and imports `SessionMenu` -island for the dropdown/sign-out action. - -## What appears in the app after `netscript plugin install @netscript/plugin-auth` +## Org/member management — future capability, named prerequisite -- `/auth/org`, `/auth/org/members`, `/auth/account` serve (typed refs: - `routes.plugins.auth.org.href()`). -- "Organization" appears in the topbar nav; the session widget appears at `app.topbar.end`. -- Nothing app-owned changed. Sign-in appears only when the user opts into the starter. +The org console rev 1 sketched requires backend surface that must ship first: an +**`auth-org` capability** — versioned procedures (org get/list, members list/invite/remove, +role set) defined in `plugin-auth-core/contracts` and implemented per adapter (better-auth +`organization` plugin; workos organizations; kv-oauth: not supported → capability absent). Once +that contract exists, the frontend story is the one rev 1 showed: live routes `/auth/org`, +`/auth/org/members`, a `MemberTable` island, nav entry, **capability-degraded per adapter** via +`ctx.client.capabilities` — one frontend across all adapters, no sniffing. The frontend +contribution layer needs nothing new for it; it waits only on the backend contract. (This +sequencing is recorded as consumer-roadmap input, not part of the v1 dogfood.) -## Adapter note +## After `netscript plugin install @netscript/plugin-auth` -Org/member management renders capability-degraded per backend: better-auth `organization` plugin -and workos organizations expose management procedures; kv-oauth has none → the org routes render -the empty-capability state (procedure presence is discoverable through the auth contract, not -sniffed in UI code). This keeps one frontend across all three adapters — harmonized-ports law. +`/auth/account` serves; the session widget appears at `app.topbar.end`; typed ref +`routes.plugins.auth.account.href()`. Sign-in appears when the user opts into the starter. +Nothing app-owned changed. diff --git a/.llm/runs/plan-frontend-contrib--seed/design/examples/deploy.md b/.llm/runs/plan-frontend-contrib--seed/design/examples/deploy.md index 489b9ea31..fd611938c 100644 --- a/.llm/runs/plan-frontend-contrib--seed/design/examples/deploy.md +++ b/.llm/runs/plan-frontend-contrib--seed/design/examples/deploy.md @@ -1,16 +1,18 @@ -# Worked example — Deploy plugin (draft) +# Worked example — Deploy plugin (draft, rev 2) -> **Draft — design document only.** The freshest consumer: branch `plan/deploy-plugin` (stage-A -> bootstrap) redefines deploy as a plugin so it can "contribute to every layers (even frontend -> soon)" — owner-ratified intent quoted in its kickoff. This example shows the two cloud seams -> that intent implies, expressed against this layer's API. It is a design input to the deploy -> seed run, not a commitment on its behalf. +> **Draft — design document only.** Rev 2 integrates adversarial finding S-15: the uniform op +> set is **seven** operations — `plan/emit`, `up`, `down`, `status`, `logs`, `rollback`, +> `secrets` (`.llm/harness/archetypes/ARCHETYPE-7-deploy-target-adapter.md:50-63`) — and rev 1's +> core-imports-adapter-islands composition reversed the adapter dependency direction. This +> example is a design input to the deploy seed run, not a commitment on its behalf. ## Assumed shape (from ARCHETYPE-7 + the deploy-plugin direction) -`plugin-deploy` (core) + per-target adapter packages (`deploy-cloudflare`, `deploy-aws`, -`deploy-vercel`) behind the deploy-target port with the uniform op set -`plan/emit/up/down/status/logs` (`.llm/harness/archetypes/ARCHETYPE-7-deploy-target-adapter.md`). +`plugin-deploy` (core) + per-target adapter packages behind the deploy-target port with the +uniform **seven-op** contract. The v1 console explicitly versions a **read-only surface** +(`status`, `logs`, `plan` preview) plus confirm-gated `up`/`down`; `rollback` and `secrets` +surface as capability-gated sections that adapters declare support for — an adapter without +`rollback` renders the absent-capability state, preserving the parity law. ## Seam 1 — LIVE: the deploy console @@ -49,12 +51,15 @@ equivalent** (`netscript deploy cloudflare up --env staging`) — the NetScript from the dashboard family's action design; on the app surface this is a plugin-internal pattern, formalized as an `ActionContribution` kind only in the dashboard family. -**Cloud-specific console surface** comes from the adapter packages as *published islands* the -core console composes (plugin-to-plugin composition through explicit exports — -`04-host-runtime.md §10`): `deploy-cloudflare` exports a `WorkersKvBrowser` island, `deploy-aws` -a `QueueDepthPanel`. Core's console imports lazily per registered target; an absent adapter means -an absent section. If adapters later become standalone plugins, each ships its own -`frontend/mod.ts` and the composition moves to zones — both paths are supported by the layer. +**Cloud-specific console surface — adapters contribute, core consumes (S-15).** Rev 1 had core +lazily import adapter islands, which reverses the adapter boundary (core would know every +target, and third-party targets would need a core change). Corrected composition: the deploy +plugin's console publishes **deploy-family zones** (`deploy.console.target-panels`, +`deploy.env.detail.sidebar`) in its `HostSurfaceDescriptor` extension, and each target adapter +ships its own frontend manifest contributing panels into those zones (`deploy-cloudflare` → a +`WorkersKvBrowser` panel; `deploy-aws` → a `QueueDepthPanel`). Core imports **no** target +module; it renders its zone registry. A third-party target plugin gets console presence the +same way — the exact dogfood of this layer's zone mechanism, one level down. ## Seam 2 — SCAFFOLDED STARTER: cloud-optimized frontend seams diff --git a/.llm/runs/plan-frontend-contrib--seed/drift.md b/.llm/runs/plan-frontend-contrib--seed/drift.md index c7f36a023..cb04670a8 100644 --- a/.llm/runs/plan-frontend-contrib--seed/drift.md +++ b/.llm/runs/plan-frontend-contrib--seed/drift.md @@ -7,3 +7,4 @@ Append-only. Severity: `minor` | `significant` | `architectural`. | 1 | 2026-07-18 | minor | Kickoff stop-lines forbid opening the seed-run draft PR that `workflow/seed-run.md` stage A prescribes. Commit trail is direct commits on `plan/frontend-contrib` (explicit refspec push); PR opening is deferred to the supervisor. | Recorded in `supervisor.md` § overrides; run proceeds PR-less. | | 2 | 2026-07-18 | minor | Kickoff replaces seed-run stages F/G/H with a custom pipeline (Codex GPT-5.6 Sol high adversarial → Kimi K3 docs pass), all supervisor-dispatched. This session stops at `STAGE-COMPLETE: generator`; no self-arranged evals. | Recorded in `supervisor.md`; conforms to lane-policy invariants (generator ≠ evaluator). | | 3 | 2026-07-18 | minor | Stage-B discovery ran on Claude Opus 4.8 Explore sub-agents (4-way fan-out) instead of Tier-C committed workflows. Rationale: `subagent-model-routing` standing rule (Fable prohibited for swarm use; Opus is the swarm lane) and CLAUDE.md's "workflows are an expensive supervisor accelerator" caveat. Findings are integrated and re-cited in `research.md`; load-bearing claims were spot-verified by the supervisor session (Fresh 2.3.3 `App.mountApp`/`Builder.registerIsland` verified directly via `deno doc`/jsr source). | Recorded here; citations in `research.md` are the audit trail. | +| 4 | 2026-07-19 | minor | Stage-2 adversarial pass dispatched at **Sol · high** per the kickoff's explicit lane spec, not the generic `review_claude` default (Sol · xhigh) in `lane-policy.md`. Owner authorized dispatch in-turn ("proceed and dispatch to codex first"). Launch via `launch-codex-slice.ts` on this worktree (Linux-local); thread `019f7883-ccf0-7820-aa36-3bd90b82ac05`; brief committed at `briefs/adversarial-sol-brief.md`. Worktree's inherited `origin/main` upstream unset to satisfy launcher push-safety (explicit-refspec pushes only). | Kickoff is the owner directive of record; recorded here + `supervisor.md`. | diff --git a/.llm/runs/plan-frontend-contrib--seed/plan.md b/.llm/runs/plan-frontend-contrib--seed/plan.md index 3b9b23d7e..21fb766b9 100644 --- a/.llm/runs/plan-frontend-contrib--seed/plan.md +++ b/.llm/runs/plan-frontend-contrib--seed/plan.md @@ -1,92 +1,100 @@ -# Plan — plan-frontend-contrib--seed (generator draft) +# Plan — plan-frontend-contrib--seed (generator draft, rev 2) -> **Status: generator draft.** Locked = this session's recommendation with rationale; nothing is -> owner-ratified. Pending: Codex GPT-5.6 Sol adversarial pass → integration → Kimi K3 docs/API -> pass → owner review. Drafts only; no board mutations; no product code. +> **Status: generator draft, post-adversarial.** Rev 2 integrates the Sol stage-2 findings +> (`adversarial-sol.md`; per-finding dispositions in `adversarial-triage.md` — all 20 accepted, +> 7 blockers). Pending: Kimi K3 docs/API pass → owner review. Drafts only; no board mutations; +> no product code. ## Scope Design (not implement) the frontend contribution layer: contracts, discovery/registry, host runtime, scaffolding/CLI, DX, and four worked consumers. Deliverables are -`design/canonical/00–06` + `design/examples/{dashboard,auth,ai,deploy}` + this plan. +`design/canonical/00–06` (rev 2) + `design/examples/*` (rev 2) + this plan. ## Locked design decisions (D#) — rationale in the canonical docs -| # | Decision | Where | +| # | Decision (rev-2 state) | Where | | --- | --- | --- | -| D1 | New Archetype-1 contract package `@netscript/plugin-frontend-core` owns the vocabulary; depends on neither fresh nor fresh-ui; component refs are module specifiers (data) | 01 | -| D2 | Core `@netscript/plugin` gains only a pointer axis (`FrontendContributionRef` + `.withFrontend()` + manifest block) — resolves OQ-12: first-class axis, zero core vocabulary | 01, 06 | -| D3 | Base family v1 = route / island / zone / nav / theme; dashboard-only kinds (panel/action/ai-tool/entity-tab/home-card) are a family EXTENSION owned by the future `plugin-dashboard-core` | 01, examples/dashboard | -| D4 | Discovery = manifest pointer + generated registry (`.netscript/generated/frontend.*`) via the existing axis-emitter pipeline; runtime registration rejected (carried verdict) | 03 | -| D5 | Islands register by module specifier on both build paths (`islandSpecifiers` / `registerIsland`) — plugin authors import islands directly, zero wrapper API | 02, 04 | -| D6 | Routes mount as lazy sub-Apps via upstream `App.mountApp`; plugin-preferred `base` + host remap + generate-time collision errors | 04 | -| D7 | Two delivery models, both first-class: live (package-served, default) vs scaffolded starter (app-owned, via `AppTarget` on `ScaffolderContext`); rule: user-edits-it → scaffold, plugin-owns-it → serve | 00, 05 | -| D8 | Data path: server = typed client + `ctx.host.serviceUrl()`; client = same client via `/api/plugins/<id>/*` proxy middleware; `requires` declares the audit surface | 04 | -| D9 | `PluginHostState` is the only host-state seam plugin pages may assume (portability across app/dashboard hosts) | 04 | -| D10 | Theming: `--ns-*` semantic vars only, CSS layered (`@layer ns-plugins`) + `[data-ns-plugin]` scoping; DTCG token merge deferred | 04 | -| D11 | App surface trust = T0 (installed plugins already run server code); containment = lazy mounts, zone error boundaries, quarantine states; T1/T2 stay dashboard-scope | 04 | -| D12 | Contract versioning: `contracts/v1` subpath + `contract: 'v1'` handshake + install-time `deno check` gate + doctor + quarantine-not-crash (carried verbatim from ratified dashboard design §4) | 01, 03 | -| D13 | Scaffold templates wire the layer by default (registry import, islandSpecifiers feed, PluginZone placements, nav feed, css import) so `plugin install` produces visible surface with zero app edits | 05 | -| D14 | Explicit manifest is the contract; file-convention generator (`netscript generate frontend`) is phase-2 sugar that also maintains JSR explicit exports | 02 | - -## Owner forks (F#) — decisions reserved for ratification, none silently taken - -| # | Fork | Options | Seed recommendation | +| D1 | `@netscript/plugin-frontend-core` owns the vocabulary; **serializable types/schemas only** — all runtime helpers (definePluginPage, pluginApi, route normalizer) live in `@netscript/fresh/plugins` (S-5) | 01, 04 | +| D2 | Core `@netscript/plugin` gains only a pointer axis — unchanged | 01, 06 | +| D3 | **Envelope + family model** (S-7): stable `FrontendManifestEnvelope` + per-family payload schemas with `(family, major)` windows; `app` family v1 = route/island/zone/nav/theme; dashboard = its own family sharing envelope/pipeline/identity, NOT a widened union | 01 | +| D4 | Discovery = manifest pointer + generated **transactional replace-set** (stage → check incl. `frontend.check.ts` → atomic swap; deterministic empty emissions on removal) (S-11) | 03 | +| D5 | Islands by module specifier on both build paths — **API verified, behavior proof-gated [P3]** (S-3): clean-cache/JSR/HMR/CSS matrix must pass before the island contract freezes | 02, 04 | +| D6 | Routes mount as sub-Apps in a **post-fsRoutes composition phase** with fixed order (middleware→routes), generated **literal lazy loaders + normalizeFreshRouteModule**, plugin `_layout` rejected in v1, child App-level commands stripped (S-1, S-2); proof [P1]/[P2], fallback = prefixed `app.route()` without `mountApp` | 04 | +| D7 | Two delivery models (live vs scaffolded starter via `AppTarget`); starters never fabricate backend surface (S-13 law) | 00, 05, examples | +| D8 | Client data path = **generated deny-by-default procedure gateway** (S-6), replacing the rev-1 wildcard proxy; AI durable streaming stays on its specialized adapter; gateway is its own reviewed wave with threat model [P5] | 04 | +| D9 | Host seam split: server `PluginRequestContext` (ports/functions ok) vs serializable `PluginClientContext` (island-safe); auth supplies a **principal port**, never a concrete type in the base contract (S-9) | 01, 04 | +| D10 | Theming: `--ns-*` only; **host-owned layer-order prelude**, per-plugin portal root, copy-mode `url()` rewrite requirement (S-12); DTCG merge deferred | 04 | +| D11 | App surface trust = T0; containment contract is **data-phase + client-side + route onError** — SSR render throws are documented page failures, not "contained" (S-4) | 04 | +| D12 | Versioning: `(family, major)` handshake + install `deno check` gate + doctor + quarantine (reserved for window mismatch/load failure — never for known-but-unmounted surfaces, S-10) | 01, 03 | +| D13 | Scaffold templates wire the layer by default; hosts publish a versioned `HostSurfaceDescriptor` (zones/navGroups/reservedPaths/family windows) — zone growth is host data, not schema change (S-10) | 01, 05 | +| D14 | Explicit manifest is the contract; **export-map maintenance + `netscript plugin dev` are phase 1** (S-16); convention-derived contribution lists remain phase-2 sugar | 02, 05 | +| D15 | Identity quartet (packageName / pluginKind / installationId / mountId); all generated keys derive from mountId (S-8) | 01 | +| D16 | Quality contract: per-plugin host-fixture test kit (`./testing`) + production budgets recorded in the manifest (S-18) | 05 | + +## Owner forks (F#) — re-triaged per S-20: technical invariants removed, policy retained + +Resolved by evidence (no longer forks): ~~F4 zone-set additivity~~ (host-descriptor data — +D13); ~~F6 generator timing~~ (exports phase 1 — D14); F7's *mechanism* (ai chat architecture +fixed by S-14 — durable-session runtime). + +| # | Fork (genuine policy) | Options | Seed recommendation | | --- | --- | --- | --- | -| F1 | Core pointer axis | (a) `.withFrontend()` pointer in `@netscript/plugin` (b) manifest-only, core untouched | **(a)** — the axis is doctrine-anticipated; pointer keeps thinness | -| F2 | Route mount policy | (a) plugin base + host remap (b) forced `/plugins/<id>` namespace on app surface | **(a)** — `/auth/org` beats `/plugins/auth/org` for end users; collisions caught at generate time | -| F3 | Contract package name | (a) `@netscript/plugin-frontend-core` (b) fold into `@netscript/fresh` (c) `@netscript/frontend-core` | **(a)** — sibling-symmetric with `plugin-*-core`; keeps fresh-free contracts | -| F4 | v1 zone set | minimal 4 (`topbar.end`, `dashboard.panels`, `home.cards`, `footer`) vs larger | **minimal 4** — zones are additive-minor later | -| F5 | Theme contributions | (a) CSS overlay only (b) + DTCG token-file merge into `tokens:build` | **(a)** at v1 | -| F6 | Convention generator timing | phase 1 vs phase 2 | **phase 2** — explicit form is the contract; sugar follows proof | -| F7 | First-party dogfood set for phase 1 | one plugin (ai chat) vs four capability panels | **ai + workers** (one live route + one zone panel) — proves both surfaces cheaply | -| F8 | Milestone placement | beta.12 … beta.14 vs stable train | defer — depends on PM #510 (beta.12) and dashboard epic sequencing; note deploy epic (#823 family) wants this layer early | - -## Phasing (implementation waves, post-ratification — each wave is normal run-loop work) - -1. **Wave 1 — contracts + spine**: `plugin-frontend-core` (contracts/v1, defineFrontend, - definePluginPage), pointer axis in `@netscript/plugin`, manifest block, registry emissions, - `@netscript/fresh/plugins` glue (mount, proxy, nav, PluginZone, withPluginIslands), - `defineFreshApp.frontend`, vite feed, scaffold template wiring, dogfood per F7. - Gate: extended `scaffold.runtime` assertions (05 §5) + SCOPE-frontend browser gates. -2. **Wave 2 — DX + lifecycle**: `plugin new --with frontend`, `generate frontend` convention - generator + exports maintenance, `generate frontend-wiring` adoption verb, doctor `frontend` - check, quarantine render states, `plugin resource add --app` (`AppTarget` seam). -3. **Wave 3 — consumers**: auth frontend (starter signin + live org console + session widget), - ai frontend (live chat + assist launcher; starter repositioned), remaining first-party panels; - deploy + dashboard epics consume the layer in their own runs. - -## Gates & validation map (per `gates/archetype-gate-matrix.md` + SCOPE-frontend) - -- `plugin-frontend-core`: Arch-1 column (F-1/5/6/7/8/10/11/12/14–19), `deno doc --lint`, - jsr-audit at publish wave. -- `packages/fresh` `./plugins`: Arch-3 column + SCOPE-frontend (route renders, browser - validation, loading/empty/error states, responsive) + consumer-import validation. -- `packages/cli`: Arch-6 + relevant F-CLI-*; scaffold-static; extended `scaffold.runtime` E2E - (install→render→hydrate→uninstall) at merge-readiness only. -- Plugins: Arch-5 + `quality:scan` + `arch:check` per the #745 lesson (wrappers alone are not a - verdict). - -## Risk register - -| Risk | Exposure | Mitigation in design | +| F1 | Core pointer axis | (a) `.withFrontend()` pointer in core (b) manifest-only | **(a)** | +| F2 | Route mount **default policy** (mechanics now fixed: full pattern/reserved-path/basePath collision rules, 03 §3) | (a) plugin-preferred base + host remap (b) forced `/plugins/<mountId>` namespace | **(a)** — `/auth/account` beats `/plugins/auth/account`; collisions are caught deterministically either way | +| F3 | Contract package name | (a) `@netscript/plugin-frontend-core` (b) fold into `@netscript/fresh` (c) `@netscript/frontend-core` | **(a)** | +| F5 | Theme contributions | (a) CSS overlay only (b) + DTCG token merge | **(a)** at v1 | +| F7 | First-party dogfood set for Wave 1 | (a) workers zone panel + console route (b) + ai durable chat (c) all four capability plugins | **(a) then (b)** — workers exercises zone+route+island+gateway; ai adds the specialized-stream pattern | +| F8 | Milestone placement | beta.12 … beta.14 vs stable train | defer — sequencing vs PM #510 (beta.12), dashboard epic (beta.13), deploy epics | +| F9 | **SSR zone isolation** (new, from S-4): accept the documented containment contract, or fund a designed isolated-render protocol later | accept v1 contract / fund protocol | **accept v1 contract**; protocol only on demonstrated need | + +## Phasing (rev 2 — Wave 0 inserted per S-19) + +**Wave 0 — five disposable proofs (no public contracts frozen until all pass):** + +| P# | Proof | Kills/confirms | +| --- | --- | --- | +| P1 | mounted-app ordering: host `_app`/nested layouts/basePath × plugin middleware/404 × two plugins | S-1 / D6 | +| P2 | literal lazy loaders + `normalizeFreshRouteModule` (handlers, config, css, error shape) | S-2 / D6 | +| P3 | dependency islands matrix: clean cache × {local-source, jsr} × {dev, prod build+SSR+hydrate}, Preact identity, dependency CSS, edit loop; plugin-vite pin/compat | S-3 / D5 | +| P4 | SSR failure containment fixtures (sync + async zone throw; resolver-catch; route onError; client boundary) | S-4 / D11 | +| P5 | gateway threat model + SSE abort/reconnect semantics on one real procedure | S-6 / D8 | + +**Wave 1 — narrow spine** (after Wave 0): envelope/schemas + identity + `frontend.check.ts` +pipeline + transactional emissions; ONE route + ONE island + ONE zone via the workers dogfood; +scaffold template wiring; host descriptor. **Wave 1b — gateway** (own reviewed wave). **Wave 2 — +DX + lifecycle**: `plugin new --with frontend`, `plugin dev`, doctor checks, quarantine states, +`plugin resource add --app` (AppTarget), test kit + budgets. **Wave 3 — consumers**: auth v1 +(starter signin + account + session widget), ai durable chat, remaining panels; deploy + +dashboard epics consume the layer in their own runs (auth-org capability is a named backend +prerequisite, not this layer's work). + +## Gates & validation map + +Per-archetype columns as rev 1, plus (S-18): the `./testing` host-fixture kit (schema, +serialization round-trip, SSR, hydration, browser, a11y/keyboard, base-path, local+JSR, +install/remove) is a required gate for every frontend-contributing plugin; production budgets +(initial JS, chunks, CSS bytes, island count, render/resolver deadlines) recorded per plugin and +asserted by the kit; `scaffold.runtime` extension for merge-readiness only. + +## Risk register (rev 2) + +| Risk | Exposure | Mitigation | | --- | --- | --- | -| Vite island registration from jsr/workspace specifiers has unproven edge cases (dedupe, HMR) | Wave 1 spine | primitive verified upstream (`mod.ts:211-214`); Wave 1 starts with a spike slice; non-vite builder path is the fallback proof | -| Dep-CSS `@import` through vite | theming | copy-into-generated fallback predeclared (03 §2) | -| JSR explicit-exports friction for island/route modules | authoring | generator-maintained exports (D14); documented interim rule | -| SSE/streaming through the plugin proxy | ai example | existing stream-proxy precedent; keep proxy thin (`app.use` passthrough) | -| Route/base collisions with user apps | UX | generate-time structured errors + host remap (D6) | -| Scope creep into dashboard family | this seed | D3 boundary + out-of-scope table (00) | -| Fresh 2.x API drift (pre-3.0 churn) | spine | wrap-points concentrated in `@netscript/fresh/plugins`; upstream pin `^2.3.3` | +| Wave-0 proofs fail (esp. P3 islands, P1 mount ordering) | contract shape | proofs are disposable and FIRST; fallback mount mechanism named (D6); JSR island mode can ship later than local-source without changing contracts | +| Gateway scope creep | security wave | deny-by-default generation from contract metadata only; no generic forwarding, ever (D8) | +| plugin-vite `^1.0.8` drift | islands | pin or compat-test in P3; version fact lives in the registry emission | +| Dep-CSS `@import` vs copy fallback | theming | url() rewrite requirement recorded (D10); asset contract deferred as debt | +| Schema evolution mistakes | contracts | envelope/family negotiation tests (old-host/new-plugin × new-host/old-plugin) are Wave-1 deliverables (D3) | +| Consumer examples drifting from backend reality | credibility | S-13/14/15 corrections landed; rule: examples cite the real contract file+lines they compose | +| Scope creep into dashboard family | this seed | D3 boundary — dashboard is a sibling family on the shared envelope | -## Debt candidates (file on implementation, not now) +## Debt candidates -See `design/canonical/06-doctrine-fit.md §4` (token-only CSS lint, tailwind content scan, DTCG -merge, island-props check, zone caps). +`design/canonical/06-doctrine-fit.md §4` (now includes AssetContribution, SSR isolation +protocol, message catalogs). ## Evidence base -`research.md` (all claims cited); prior-art: -`dashboard-design--orchestrator/analysis/plugin-extension-architecture.md` (generalized by D3), -roadmap-expansion A-dashboard §9 / OQ-12 (resolved by D2), deploy-plugin kickoff (owner intent for -seam 2), doctrine 07 (axis table row made real). +`research.md`; `adversarial-sol.md` + `adversarial-triage.md` (stage-2 record); prior-art as +rev 1 (dashboard extension architecture, A-dashboard §9/OQ-12, deploy kickoff, doctrine 07). diff --git a/.llm/runs/plan-frontend-contrib--seed/worklog.md b/.llm/runs/plan-frontend-contrib--seed/worklog.md index c55610c3e..462730ecf 100644 --- a/.llm/runs/plan-frontend-contrib--seed/worklog.md +++ b/.llm/runs/plan-frontend-contrib--seed/worklog.md @@ -55,3 +55,24 @@ this session (resumed with `context-pack.md`) integrates findings; then the Kimi pass. This session self-arranged no evals. STAGE-COMPLETE: generator + +## 2026-07-19 — Stage 2: adversarial pass dispatched + integrated + +- Owner authorized pipeline dispatch in-turn. Sol high launched via + `launch-codex-slice.ts` (Linux-local), thread `019f7883-ccf0-7820-aa36-3bd90b82ac05`; + findings committed by the reviewer as `adversarial-sol.md` (`4d2647c8`). +- Review quality: high — 20 findings (7 blockers), upstream claims verified against jsr sources. + **All 20 accepted** (two harmless citation slips noted); dispositions in + `adversarial-triage.md`. +- Integration (rev 2 across the design): envelope+family versioning model (S-7), identity + quartet (S-8), server/client context split (S-9), HostSurfaceDescriptor + discriminated nav + (S-10), runtime sugar relocated to `@netscript/fresh/plugins` (S-5), post-fsRoutes composition + phase + literal route loaders + normalizer (S-1/S-2), islands reclassified proof-gated (S-3), + honest SSR containment contract (S-4), wildcard proxy replaced by generated deny-by-default + gateway (S-6), transactional replace-set emissions + `plugin remove` semantics (S-11), CSS + layer-order prelude/portal roots/url() rule (S-12), examples corrected to real backend + surfaces (S-13/14/15), exports+`plugin dev` moved to phase 1 (S-16), i18n/a11y/CSP seams + (S-17), test kit + budgets (S-18), **Wave 0 five-proof phasing** (S-19), forks re-triaged + (S-20; F4/F6 resolved, F9 added). + +STAGE-COMPLETE: adversarial-integration From 29b7d8051a312c2ee177e71e14f723a799aba9bd Mon Sep 17 00:00:00 2001 From: Rickylabs <eric.chautems@gmail.com> Date: Sun, 19 Jul 2026 06:27:55 +0200 Subject: [PATCH 05/11] plan(frontend-contrib): stage-3 Kimi docs brief Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015fc7SaneZJ9mzrAhRW4tma --- .../briefs/kimi-docs-brief.md | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 .llm/runs/plan-frontend-contrib--seed/briefs/kimi-docs-brief.md diff --git a/.llm/runs/plan-frontend-contrib--seed/briefs/kimi-docs-brief.md b/.llm/runs/plan-frontend-contrib--seed/briefs/kimi-docs-brief.md new file mode 100644 index 000000000..935b3db53 --- /dev/null +++ b/.llm/runs/plan-frontend-contrib--seed/briefs/kimi-docs-brief.md @@ -0,0 +1,57 @@ +use harness + +# Stage 3 — Public docs & API story (Kimi K3, docs-first concretion) + +You are the **stage-3 docs agent** for seed run `.llm/runs/plan-frontend-contrib--seed/` on +branch `plan/frontend-contrib`. The frontend contribution layer has been designed (rev 2, +post-adversarial). Your job — the owner's rationale, verbatim intent: *forecasting the docs +makes the public API concrete and forces DX-first implementation.* You write the public-facing +documentation as if the feature had shipped. Where writing the docs exposes an API wart, you +record it — you do not change the contracts. + +## SKILL + +Read in order: `.agents/skills/netscript-harness/SKILL.md` (context only — you produce docs +drafts, no evals), then the run docs: `design/canonical/00-overview.md` → `06-doctrine-fit.md`, +`design/examples/*.md`, `plan.md`. Treat `design/canonical/01-contracts.md` (rev 2) as the +normative API. For voice/format, look at 2-3 existing public docs pages under `docs/site/` and +the README of `packages/fresh-ui/` or `packages/plugin/`. + +## Deliverables (create exactly these files) + +1. `design/docs-story/guide-plugin-frontend.md` — the future docs-site guide **"Ship frontend + from your plugin"**, written as-if-shipped: 5-minute quickstart (the crons example), + authoring model (routes/islands/zones/nav/theme), the dev loop (`plugin new --with frontend`, + `plugin dev`, local-source vs published modes — honest about both), data access via the + gateway + typed clients, starters vs live (`plugin resource add`), theming with `--ns-*` + tokens, troubleshooting (quarantine states, doctor). +2. `design/docs-story/reference-plugin-frontend-core.md` — API reference for + `@netscript/plugin-frontend-core` (`defineFrontend`, envelope, the five `app`-family kinds, + identity, `HostSurfaceDescriptor`, contexts, `requires`, `./testing` kit) in deno-doc-style + sections with signatures + one example each. +3. `design/docs-story/reference-fresh-plugins.md` — API reference for `@netscript/fresh/plugins` + (`defineFreshApp` `frontend` option, `mountPluginFrontends`, `definePluginPage`, `pluginApi`, + `PluginZone`, `pluginNavSections`, `normalizeFreshRouteModule`, the generated gateway). +4. `design/docs-story/readme-fragments.md` — the README section each affected package gains + (plugin-frontend-core, fresh, plugin, cli), each a self-contained fragment. +5. `design/docs-story/docs-story-notes.md` — **the point of this stage**: every place where + writing the doc was awkward — a name that reads badly, a concept needing two paragraphs where + one should do, a step authors will forget, an asymmetry between kinds — as numbered notes + (K-1, K-2, …) with a concrete suggestion each. Honest and specific; the generator integrates + these next. + +## Hard rules + +- **No invented APIs.** Every symbol/field you document must exist in the rev-2 contracts docs. + Mechanisms the design marks [P1]–[P5] (proof-gated) are documented without the proof caveat in + the as-if-shipped guide BUT listed in docs-story-notes.md as pending proofs. If a doc needs an + API the design lacks, that is a K-note, not a new API. +- **Public-clean prose** in files 1–4: no internal run/process/PR references, no harness + vocabulary, no model/agent names. File 5 is internal and may reference anything. +- Files land ONLY under `.llm/runs/plan-frontend-contrib--seed/design/docs-story/`. No other + file edits; nothing under `packages/`, `plugins/`, `docs/`, `.github/`. +- Match the repo's Markdown style (100-col wrap, fenced ts/tsx blocks). +- Commit exactly once: `plan(frontend-contrib): Kimi K3 docs story (stage 3)` and push with + `git push origin HEAD:refs/heads/plan/frontend-contrib` (never bare push). If push fails, + leave the commit local and say so. +- End your final message with `DONE` (or `BLOCKED: <reason>`). From 73cbd48b7cff696d3abb752cf62a10cc807a3452 Mon Sep 17 00:00:00 2001 From: Rickylabs <eric.chautems@gmail.com> Date: Sun, 19 Jul 2026 06:58:23 +0200 Subject: [PATCH 06/11] plan(frontend-contrib): Kimi K3 docs story (stage 3) --- .../design/docs-story/docs-story-notes.md | 167 +++++++ .../docs-story/guide-plugin-frontend.md | 318 ++++++++++++ .../design/docs-story/readme-fragments.md | 148 ++++++ .../docs-story/reference-fresh-plugins.md | 207 ++++++++ .../reference-plugin-frontend-core.md | 456 ++++++++++++++++++ .../runs/plan-frontend-contrib--seed/drift.md | 1 + 6 files changed, 1297 insertions(+) create mode 100644 .llm/runs/plan-frontend-contrib--seed/design/docs-story/docs-story-notes.md create mode 100644 .llm/runs/plan-frontend-contrib--seed/design/docs-story/guide-plugin-frontend.md create mode 100644 .llm/runs/plan-frontend-contrib--seed/design/docs-story/readme-fragments.md create mode 100644 .llm/runs/plan-frontend-contrib--seed/design/docs-story/reference-fresh-plugins.md create mode 100644 .llm/runs/plan-frontend-contrib--seed/design/docs-story/reference-plugin-frontend-core.md diff --git a/.llm/runs/plan-frontend-contrib--seed/design/docs-story/docs-story-notes.md b/.llm/runs/plan-frontend-contrib--seed/design/docs-story/docs-story-notes.md new file mode 100644 index 000000000..134ba2186 --- /dev/null +++ b/.llm/runs/plan-frontend-contrib--seed/design/docs-story/docs-story-notes.md @@ -0,0 +1,167 @@ +# Docs-story notes — what writing the docs exposed + +Internal working notes from the stage-3 docs pass. Files 1–4 in this directory were written +as-if-shipped, per the stage brief: proof-gated mechanisms appear without caveats, and every place +the design made the docs awkward is recorded here instead of being patched around. Each note has a +concrete suggestion. The generator integrates these next; nothing here changes the rev-2 contracts +on its own. + +## Pending proofs the docs assert as shipped + +Per the brief, these are documented without the proof caveat in the guide and references. If a +proof fails, the corresponding public statements need revisiting — they are quoted so the fallback +doc edit is findable. + +| Proof | What the docs assert as fact | Where | +| --- | --- | --- | +| **P1** (mounted-app ordering) | The composition phase order "static files → host middleware → host fs routes/layouts → `mountPluginFrontends`" and per-plugin "middleware first, then routes"; child `App`-level commands "stripped and reported". | `reference-fresh-plugins.md` (`defineFreshApp`, `mountPluginFrontends`); `guide` Troubleshooting. | +| **P2** (literal loaders + normalizer) | `normalizeFreshRouteModule` maps `default`/`handler(s)`/`config`/`css` and throws a structured error on unknown members; generated loaders are literal specifiers. | `reference-fresh-plugins.md` (`normalizeFreshRouteModule`); `reference-plugin-frontend-core.md` (`RouteContribution`, `ComponentRef`). | +| **P3** (dependency islands matrix) | Islands register by specifier on both build paths; published mode "takes a signaled full reload rather than HMR"; `plugin dev` owns the signal; the testing kit covers "local-source and JSR-installed" modes. | `guide` (Dev loop, `./testing` row); `reference-plugin-frontend-core.md` (`IslandContribution`, `./testing`). | +| **P4** (SSR containment fixtures) | The containment contract as documented: data-phase guard → quarantine card; client boundary for islands; route-level `onError`; SSR render throw fails the page. | `guide` Troubleshooting; `reference-fresh-plugins.md` (`PluginZone`). | +| **P5** (gateway threat model) | The gateway posture list: principal-port auth, no blind credential forwarding, origin/CSRF checks, size limits, timeouts, abort propagation, manual redirects, header allowlist, per-invocation audit line, streaming only where metadata declares it. | `guide` Data access; `reference-fresh-plugins.md` (generated gateway). | + +## K-notes + +**K-1 — The worked examples still speak rev 1.** `design/examples/dashboard.md` and +`design/examples/ai.md` declare `contract: 'v1'`, `plugin: 'workers'`, and plain-string nav labels +(`nav: { label: 'Workers', … }`) — shapes rev 2 replaced with `contract: { family, major }`, +`pluginKind`, and `MessageRef` labels. Writing the quickstart meant choosing which voice was +normative; the answer was only knowable from `02-authoring-dx.md`. +**Suggestion:** re-run both examples through the rev-2 envelope before owner ratification. Docs +forecasting catches exactly this class of drift; the examples are what Wave-1 implementers will +copy. + +**K-2 — `pluginApi`'s argument is stated two ways.** `02-authoring-dx.md` shows +`pluginApi(props.client)`; `examples/dashboard.md`'s zone comment says "polls via +`pluginApi('workers')`". One takes the client context, the other a plugin id — they cannot both be +the signature. +**Suggestion:** lock `pluginApi(client: PluginClientContext): string` (the client context is what +an island legitimately holds; a raw plugin id would invite hand-typed strings). Fix the dashboard +example comment. + +**K-3 — The `frontend` option's input is shown two ways.** `00-overview.md`'s diagram reads +`frontend: fromGenerated(registry)`; `04-host-runtime.md` reads `frontend: frontendRegistry`. The +reference documented the latter (simpler, one less symbol). +**Suggestion:** pick the bare-registry form and delete `fromGenerated` from the overview diagram — +or, if the wrapper exists to adapt the generated module's shape, say what it adapts and why the +option can't take the registry directly. + +**K-4 — `ctx.redirect` appears in an example but not in the contract.** `examples/auth.md` writes +`if (!ctx.host.principal) return ctx.redirect('/auth/signin')`. `01-contracts.md` describes the +page ctx as Fresh `PageProps` + injected state; `04 §5` says `definePluginPage` is "typed over +Fresh PageProps with the injected state". Redirect is the first thing every auth-adjacent page +needs; the reference could not pin it down. +**Suggestion:** either add a typed `redirect` helper to the `definePluginPage` context in +`01-contracts.md`, or rewrite the auth example with the sugar-free idiom. Do not let the first +auth page authors write depend on an uncontracted field. + +**K-5 — Standalone nav contributions have no declared authoring path.** The keyed `defineFrontend` +form shows `routes`, `islands`, `zones`, `theme` — never a top-level `nav` array — yet +`NavContribution` is a first-class kind (an external docs link, an href to a host page). And +`NavSpec` (the inline route shorthand) shows `label`/`icon`/`group` but never says whether `order` +is supported. +**Suggestion:** pin the keyed form: either add `nav?: readonly NavContribution[]` or state that +standalone nav is declared only inside the envelope (not the keyed form). Pin `NavSpec` as +`{ label: MessageRef; icon?: string; group?: string; order?: number }` or document the omission of +`order` as deliberate. + +**K-6 — `theme` is the odd key out.** The keyed form is `routes`/`islands`/`zones` (plurals) plus +`theme` (singular) holding an array. Every author will write `themes:` once and get a schema +error. +**Suggestion:** rename the keyed field to `themes` for symmetry, or make `theme` singular in fact +(one `ThemeContribution` per plugin, multiple CSS files inside it) — the second reads better and +matches the "overlays" mental model. + +**K-7 — `contract: { family: 'app', major: 1 }` is boilerplate every author restates.** Until a +second family exists, this line is copied verbatim into every `frontend/mod.ts` with no decision +attached. Fields authors copy without thinking are fields they stop seeing. +**Suggestion:** default `contract` to `{ family: 'app', major: 1 }` inside `defineFrontend`, +keeping the field overridable for multi-family plugins. The handshake stays explicit in the +*compiled envelope*; it stops being noise in the *authoring form*. + +**K-8 — `MessageRef` is two objects deep for the most common case.** `nav: { label: { id: +'crons.nav.calendar', default: 'Cron calendar' } }` is the right shape for localization and the +wrong shape for momentum: the examples already drifted to plain strings (K-1), which predicts +author behavior. +**Suggestion:** accept a string shorthand in the keyed form that compiles to a `MessageRef` with a +derived id (`<pluginKind>.nav.<routeId>`), with the object form always available. Otherwise expect +every third-party manifest to carry `default: ''` placeholders. + +**K-9 — The `./testing` kit has no importable name.** `05 §5` and `06` specify the kit's coverage +thoroughly but name no entrypoint, so the reference page documents behavior without a single +runnable snippet — the weakest section in file 2, and reviewers will notice. +**Suggestion:** name the entrypoint in the contracts (e.g. `defineFrontendTestSuite(manifest, +options)` or a generated `frontend_test.ts` fixture the plugin's `deno test` picks up) before +Wave 2, and give the budgets block a manifest location and shape. + +**K-10 — `.withFrontend()` restates what `mod.ts` already says.** The pointer carries +`contract: [{ family: 'app', major: 1 }]`; the envelope in `frontend/mod.ts` carries the same +triple. Two sources of truth drift (bump the family in one, forget the other) and the failure +surfaces as a cryptic handshake quarantine. +**Suggestion:** derive the pointer's `contract` from the module at generate time (the manifest +stays a pure pointer), or have `plugin doctor`'s frontend check flag the drift explicitly with +both values printed. + +**K-11 — One route parameter, two syntaxes.** The manifest says `path: '/schedules/:id'`; the file +is `routes/schedules/[id].tsx`. Both are "Fresh syntax" in their own domain, but they sit one line +apart in the declaration and every author will momentarily wonder which one wins. +**Suggestion:** add a generate-time cross-check that the module path and the route pattern name +the same parameters (the machinery already cross-checks export-map presence), and one sentence in +the guide: the manifest uses Fresh *pattern* syntax, the filename uses Fresh *fs-routes* syntax. + +**K-12 — Island `id` has no stated job.** Routes need ids for nav targets and typed refs; zones +need them for `data-ns-contribution` and ordering; islands register by module specifier on both +build paths, so the id's effect is undocumented — the reference says "unique within (plugin, +family)" and nothing more. +**Suggestion:** state the purpose in `01-contracts.md` (registry identity, duplicate rejection, +doctor diagnostics, budget attribution) or drop the field from `IslandContribution`. An id that +does nothing is a field authors cargo-cult. + +**K-13 — The gateway's base path is nowhere pinned.** The docs need to tell authors what URL +`pluginApi` returns. The evidence is circumstantial: `reservedPaths` lists `'/api/plugins'`, the +identity model says the "gateway prefix" derives from `mountId`. The reference wrote +around it ("derived from the client context's `mountId`") because no contract line pins +`/api/plugins/<mountId>/…`. +**Suggestion:** pin the gateway prefix shape in `01-contracts.md` next to `FrontendIdentity` — +three moving parts (`pluginApi`, `reservedPaths`, the generated route table) silently depend on +the same string. + +**K-14 — `ComponentRef` covers CSS files.** `ThemeContribution.css` is typed `readonly +ComponentRef[]`, but a stylesheet is not a component; the reference had to describe the alias +twice ("module specifier" for components, "also CSS") and the name actively misleads in the theme +section. +**Suggestion:** rename the alias to `ModuleRef` (or `AssetRef`) in the contracts — it already +means "package-relative module specifier" everywhere it is used. + +**K-15 — Three types the docs needed are unnamed in the contracts.** The reference invented +presentation names: `FrontendDefinition` (the keyed `defineFrontend` input — only the compiled +envelope is named in `01-contracts.md`), `PluginPageProps` (the `definePluginPage` ctx — "typed +over Fresh PageProps with the injected state" names no type), and `FrontendContributionRegistry` +(the generated registry's type, whose owning module — `fresh/plugins` vs `plugin-frontend-core` — +is unpinned; `mountPluginFrontends` and `pluginNavSections` both take it). +**Suggestion:** export real names for all three. Docs and error messages both need them; "the +keyed form" is prose, not an API. + +**K-16 — Multi-family export mechanics are unspecified.** `01 §envelope` says the `./frontend` +export "is one envelope (or an array of envelopes, one per family)", but `defineFrontend`'s return +and the array form are both unshown: does the author call `defineFrontend` once per family and +export an array, or is there a variadic/array input? +**Suggestion:** pin one: `export default [defineFrontend(appDef), defineFrontend(dashboardDef)]` +reads naturally and needs no new API — but say it in the contracts, because the dashboard family +is the announced second consumer. + +**K-17 — "Quarantine" carries three user-visible meanings.** The design's diagnosis taxonomy +(unknown zone / known-but-unmounted / capacity-rejected / window-mismatch quarantine / load-failure +quarantine) is well separated internally, but an author staring at a gray card sees one word. The +guide needed a comparison table to make the states legible; that is a smell that the *product* +surface (doctor output, quarantine card copy) must carry the same precision. +**Suggestion:** make the doctor's frontend check print the taxonomy verbatim (the guide's table +can then quote it), and have the quarantine card deep-link the doctor command. Cheap, and it turns +a support question into a self-serve loop. + +## Process note for the generator + +Files 1–4 are public-clean: no run, PR, harness, proof, or model references; the `[P#]` gates +appear only above. If any K-note is accepted as a contract change, the corresponding doc file +needs the matching edit in the same pass — the guide's quickstart (K-6, K-7, K-8), both references +(K-2, K-5, K-13, K-14, K-15), and the CLI README fragment (K-17) are the touch points. diff --git a/.llm/runs/plan-frontend-contrib--seed/design/docs-story/guide-plugin-frontend.md b/.llm/runs/plan-frontend-contrib--seed/design/docs-story/guide-plugin-frontend.md new file mode 100644 index 000000000..26aa713f8 --- /dev/null +++ b/.llm/runs/plan-frontend-contrib--seed/design/docs-story/guide-plugin-frontend.md @@ -0,0 +1,318 @@ +# Ship frontend from your plugin + +A NetScript plugin is a full-stack unit. The same package that contributes services, workers, +schemas, and Aspire wiring can also contribute **pages, islands, zone components, nav entries, and +theme CSS** to any NetScript Fresh app — declared as data in a `frontend/` directory, discovered by +the CLI, and mounted by the host with zero app edits. + +You write ordinary Fresh code — the same `routes/`-and-`islands/` idiom you already know — plus one +`defineFrontend()` export. This guide walks through the whole life of a plugin frontend: scaffolding +it, declaring it, iterating on it, and fixing it when something is off. + +## Quickstart: a cron calendar in five minutes + +You need a NetScript workspace with a scaffolded app. Everything below lives in the plugin you are +about to create — the app is never edited. + +**1 · Scaffold the plugin with the frontend axis.** + +```bash +netscript plugin new crons --with frontend +``` + +The generator emits the frontend tree, registers the axis in `scaffold.plugin.json`, and seeds the +`deno.json` export map (`./frontend` plus every route and island module — tooling maintains that +list from here on, you never hand-edit it): + +```text +plugins/crons/ + frontend/ + mod.ts ← the one declaration + routes/calendar.tsx ← an ordinary Fresh route module + islands/CronCalendar.tsx ← an ordinary Fresh island (one component per file) + components/NextFiresCard.tsx + theme.css ← --ns-* overlays only +``` + +**2 · Declare what the plugin contributes.** `frontend/mod.ts` is the whole contract — validated +and frozen by `defineFrontend`: + +```ts +// plugins/crons/frontend/mod.ts +import { defineFrontend } from '@netscript/plugin-frontend-core'; + +export default defineFrontend({ + contract: { family: 'app', major: 1 }, + pluginKind: 'crons', + base: '/crons', + routes: [ + { + kind: 'route', + id: 'calendar', + path: '/calendar', + module: './routes/calendar.tsx', + nav: { label: { id: 'crons.nav.calendar', default: 'Cron calendar' }, icon: 'calendar', group: 'main' }, + }, + { kind: 'route', id: 'schedule-detail', path: '/schedules/:id', module: './routes/schedules/[id].tsx' }, + ], + islands: [{ kind: 'island', id: 'cron-calendar', module: './islands/CronCalendar.tsx' }], + zones: [ + { kind: 'zone', id: 'next-fires', zone: 'app.dashboard.panels', module: './components/NextFiresCard.tsx' }, + ], + theme: [{ kind: 'theme', id: 'theme', css: ['./theme.css'] }], + requires: { procedures: ['crons.list', 'crons.nextFires'] }, +}); +``` + +**3 · Write the page and the island.** Pages use `definePluginPage`, which types the host context +the mount layer injects. It is just Fresh underneath: + +```tsx +// plugins/crons/frontend/routes/calendar.tsx +import { definePluginPage } from '@netscript/fresh/plugins'; +import { CronCalendar } from '../islands/CronCalendar.tsx'; +import { createCronsClient } from '@acme/plugin-crons/contracts/v1'; + +export default definePluginPage(async (ctx) => { + // ctx is Fresh PageProps + typed ctx.host (server-only) and ctx.client (island-safe). + const crons = await createCronsClient(ctx.host.serviceUrl('crons-api')).crons.list(); + return ( + <section class='ns-stack ns-stack--lg'> + <h1 class='ns-heading'>Cron calendar</h1> + <CronCalendar initial={crons} client={ctx.client} /> + </section> + ); +}); +``` + +**4 · Install it into the app.** + +```bash +netscript plugin install crons --local-path ./plugins/crons +``` + +Install regenerates the app's frontend wiring, type-checks the staged output — a broken module +export fails here, with the real diagnostic, not at the first Vite build — and swaps it in +atomically. The app now serves `/crons/calendar`, the sidebar gains **Cron calendar**, the +dashboard grows a *Next fires* panel, and the theme overlay applies. No app file changed. + +**5 · Iterate.** In the plugin directory: + +```bash +netscript plugin dev +``` + +`plugin dev` watches `frontend/`, keeps the export map in sync, regenerates the host registry +atomically, and signals the app's Vite server to reload. Edit the calendar page and the change is +in front of you. + +## The authoring model + +Everything a plugin contributes is one of **five kinds**, declared in `frontend/mod.ts` and +compiled into a versioned manifest envelope. Component references are module specifiers — data — +which is what keeps the registry statically generatable. + +| Kind | What it is | Where it lands | +| ------- | ----------------------------------------------------------------- | ----------------------------- | +| `route` | A Fresh route module (page and/or handlers) mounted under `base`. | `base + path`, e.g. `/crons/calendar` | +| `island` | One interactive component per file, hydrated on the client. | Imported by routes and zones. | +| `zone` | An SSR component injected into a host-published zone. | `<PluginZone>` render points. | +| `nav` | A navigation entry with a discriminated target. | Host sidebar / topbar. | +| `theme` | CSS overlays in the `--ns-*` token vocabulary. | The aggregated stylesheet. | + +### Routes + +A route's `path` is composed **under the resolved mount base** in Fresh pattern syntax — +`path: '/calendar'` with `base: '/crons'` serves `/crons/calendar`. The `module` is a +package-relative specifier; the generated registry emits a literal lazy loader for it, so the +bundler sees a real edge. Two conveniences worth knowing: + +- `nav` on a route is shorthand: it emits a nav contribution targeting that route, with the base + path composed for you. +- Typed hrefs are generated for every contributed route — `routes.plugins.crons.calendar.href()` — + so the app and other plugins link without string concatenation. + +Plugin `_layout` modules are not supported: wrap pages in a shared component instead. A plugin +route that throws fails **that route**, host-styled — never the app shell. + +### Islands + +Islands follow the Fresh convention: one component per file. The one rule that matters crosses the +serialization boundary: + +> **Island props must be serializable data.** No functions, no `Request`, no server context. Pass +> `ctx.client` (the serializable `PluginClientContext`), never `ctx.host`. The testing kit fails a +> contribution whose island props do not round-trip. + +### Zones + +Zones are SSR render points the **host** publishes — the scaffolded app ships +`app.topbar.end`, `app.dashboard.panels`, `app.home.cards`, and `app.footer`. A zone contribution +names a zone id and an SSR component; the host renders contributions in deterministic order and +validates the zone id against its own published surface, so a typo is a generate-time error, not a +silent miss. + +Zone components receive `{ url, host, client }` server-side and may forward only `client` (or +slices of it) to islands. Keep renders pure: **fetch in the resolver, not the component body** — +see [Troubleshooting](#troubleshooting) for why. + +### Nav + +A nav contribution's target is discriminated — no ambiguous strings: + +```ts +target: { kind: 'route', routeId: 'calendar' } // same plugin; base path composed +target: { kind: 'href', href: '/settings' } // host-internal absolute path +target: { kind: 'external', href: 'https://…' } // rel="noopener noreferrer" enforced +``` + +Labels are message references (`{ id, default }`), so hosts with a message catalog can localize +them and every host always has the default text. The `group` is validated against the host's nav +groups — the scaffolded app publishes `main`. + +### Theme + +Theme contributions are CSS files written against the `--ns-*` token vocabulary and nothing else. +See [Theming with `--ns-*` tokens](#theming-with---ns--tokens) below. + +## The dev loop + +Two modes exist, and they are honestly different: + +- **Local-source mode** (the plugin lives in your workspace) is the primary authoring loop. + Route and component edits hot-reload through Vite's `watchPaths`. Island-list changes and + manifest edits are picked up by `netscript plugin dev`, which regenerates the host registry and + signals a reload. This is the mode the quickstart uses. +- **Published mode** (the plugin is installed from JSR) is immutable: packages do not change on + disk, so the loop is *publish → `netscript plugin update`*. Island edits in this mode take a + signaled full reload rather than HMR. Neither loop pretends to be the other; plan your + inner-loop work in local-source mode and treat published installs as release consumption. + +In both modes the **export map is tooling-owned**: JSR resolves only published entrypoints, so +`./frontend` and every route/island module need explicit `deno.json` exports. `plugin new --with +frontend` seeds them; `plugin dev` and `netscript generate frontend` keep them in sync. + +## Data access: typed clients and the gateway + +**Server-side code does not need a proxy.** Route handlers and zone resolvers call typed clients +directly against the host's service resolution: + +```ts +const crons = await createCronsClient(ctx.host.serviceUrl('crons-api')).crons.list(); +``` + +`ctx.host` also carries the principal port (supplied by the auth plugin when installed, `null` +otherwise), the resolved mount base, the CSP nonce seam, and the request's abort signal. + +**Browser code goes through the generated gateway.** The gateway is a deny-by-default route table +generated from your `requires.procedures` — one route per granted procedure, derived from your +plugin's own versioned contract metadata. Nothing else is reachable: no wildcard forwarding, ever. +It authenticates server-side through the host principal, enforces origin/CSRF checks, size limits, +timeouts, and abort propagation, and writes a structured audit line per call. + +Islands reach it through `pluginApi`, which turns the client context into the gateway base URL: + +```tsx +// plugins/crons/frontend/islands/CronCalendar.tsx +import { useSignal } from '@preact/signals'; +import { pluginApi } from '@netscript/fresh/plugins'; +import type { PluginClientContext } from '@netscript/plugin-frontend-core/contracts/v1'; + +export function CronCalendar(props: { initial: readonly CronEntry[]; client: PluginClientContext }) { + const entries = useSignal(props.initial); + async function refresh() { + entries.value = await createCronsClient(pluginApi(props.client)).crons.list(); + } + // … +} +``` + +`requires` is the actual allowlist, not an audit comment — a procedure you do not declare has no +gateway route. Granted capabilities also surface on `ctx.client.capabilities`, so a plugin can +degrade its UI cleanly when a host (or an adapter) does not provide something. + +## Starters vs live: two delivery models + +> **If the user will edit it, scaffold it; if the plugin owns it, serve it.** + +- **Live (default).** The app imports the plugin's published frontend modules; updates ship with + the package version. Right for consoles, panels, and widgets — surfaces the plugin *operates*. +- **Scaffolded starter (opt-in).** Files are generated into the app and become app-owned. Right + for surfaces the user is expected to customize — a sign-in page, a cloud-optimized route. + +Starters are delivered through the resource channel: + +```bash +netscript plugin resource add auth signin --app . +# → routes/auth/signin.tsx, routes/auth/callback.tsx (app-owned from this moment) +``` + +Starters compose existing backend procedures — they never fabricate backend surface. Updates +report drift, never overwrite, and each generated file carries a provenance header so `plugin +doctor` can report an orphaned starter without ever deleting it. A plugin may ship both models for +the same feature: the auth plugin serves the account page live and scaffolds the sign-in page. +Start live, eject to a starter when you need to own the pixels. + +## Theming with `--ns-*` tokens + +Plugin CSS speaks one vocabulary: the `--ns-*` semantic custom properties. That buys three things: + +1. **The app always wins.** The generated stylesheet opens with a host-owned layer prelude + (`@layer ns-app, ns-plugins;`), so plugin CSS loses to app CSS by declaration, not by luck. +2. **Scoping is automatic.** Zones and plugin route trees render under + `[data-ns-plugin='<mountId>']` wrappers. Overlays and portals escape wrappers, so the host + provides a per-plugin portal root under `document.body` — fresh-ui overlay primitives mount + there via the client context. +3. **Themes come for free.** Token overlays inherit light/dark and any host theme swap; raw hex + values are out of contract. + +```css +/* plugins/crons/frontend/theme.css */ +[data-ns-plugin='crons'] .cron-calendar { + background: var(--ns-surface-raised); + color: var(--ns-fg); + border-color: var(--ns-border); +} +``` + +## Troubleshooting + +**Install fails with a type error.** The staged generation is type-checked before it swaps in — +including a check module that imports every referenced route, island, and CSS module. A missing +deep export fails install with the real diagnostic; run `netscript plugin dev` (or +`netscript generate frontend`) in the plugin to repair the export map, then reinstall. + +**A contribution renders as a quarantine card.** Quarantine is a render state, not a crash, and it +is reserved for two causes: the plugin's `(family, major)` contract window falls outside what the +host supports, or a module failed to load. `netscript plugin doctor` prints the mismatch and the +remediation (usually: update the plugin, or update the host). + +**A zone contribution silently does not appear.** Three diagnoses look alike but differ: + +| Diagnosis | Meaning | Disposition | +| ---------------------- | -------------------------------------------------- | --------------------------------- | +| Unknown zone | The id is not in the host's published surface — a typo. | Generate-time **error**. | +| Known-but-unmounted | The zone exists in the family but this host does not mount it. | **Informational**; contribution skipped, never quarantined. | +| Capacity-rejected | The zone's capacity is exhausted. | Deterministic overflow report naming winners and losers. | + +**A page 500s when a zone throws.** SSR render-time throws in zone components are not containable +by a component boundary — they fail the page response. That is why the rule exists: resolve data +in the resolver (host-side, before render, inside a guard), keep the render pure. Failed +resolutions render the quarantine card; pure renders cannot throw on data. + +**Nav entry missing or route link broken.** Nav targets of kind `route` must name a route id from +the same plugin; unknown ids are generate-time errors. Use the generated typed refs +(`routes.plugins.crons.calendar.href()`) instead of hand-built strings. + +**`netscript plugin doctor` is the front door.** Its frontend check validates the +envelope/window handshake, zone ids against the host descriptor, export-map presence, and +orphan/stale generated output — a registry entry with no installed plugin, a CSS import with no +file, a starter whose plugin is gone. + +## See also + +- `@netscript/plugin-frontend-core` reference — the contract vocabulary: `defineFrontend`, the + envelope, the five kinds, identity, host contexts, `requires`, and the testing kit. +- `@netscript/fresh/plugins` reference — the runtime surface: the `frontend` option, + `definePluginPage`, `pluginApi`, `PluginZone`, `pluginNavSections`, and the generated gateway. +- `@netscript/fresh-ui` — the `--ns-*` token vocabulary and component runtime. diff --git a/.llm/runs/plan-frontend-contrib--seed/design/docs-story/readme-fragments.md b/.llm/runs/plan-frontend-contrib--seed/design/docs-story/readme-fragments.md new file mode 100644 index 000000000..8c6b4481c --- /dev/null +++ b/.llm/runs/plan-frontend-contrib--seed/design/docs-story/readme-fragments.md @@ -0,0 +1,148 @@ +# README fragments + +Each fragment below is a self-contained section written to drop into the named package's README. + +--- + +## Fragment: `@netscript/plugin-frontend-core` — opening sections + +````markdown +**The contract vocabulary for NetScript plugin frontends: serializable types, Standard-Schema +validation, and the `defineFrontend` builder that turns a plugin's `frontend/` directory into a +versioned manifest envelope.** + +A NetScript plugin is a full-stack unit: alongside services and workers it can contribute pages, +islands, zone components, nav entries, and theme CSS to any Fresh host. This package defines what +those contributions *are* — as data. It deliberately contains no Fresh, no Preact, and no runtime +helpers; everything that touches the framework lives in `@netscript/fresh/plugins`. Because +component references are module specifiers, host tooling can discover, validate, and type-check a +plugin's frontend without executing plugin code. + +## Why authors use it + +- **One declaration file** — `defineFrontend` validates the whole `frontend/` story at once and + freezes it, so a malformed contribution fails in the plugin, not inside a host. +- **An envelope that can evolve** — a stable envelope plus per-family payload schemas with + `(family, major)` windows: new optional fields are minor, new kinds are a new major, and hosts + quarantine what they cannot serve instead of crashing. +- **Host-negotiated surfaces** — zones and nav groups are host-published data, so hosts grow new + contribution points without contract changes. +- **A test kit, not vibes** — `./testing` mounts the plugin against a generated host fixture: + schema/resolution checks, island-props serialization round-trip, SSR, hydration, browser smoke, + accessibility, and production budgets. + +## Quick example + +```ts +import { defineFrontend } from '@netscript/plugin-frontend-core'; + +export default defineFrontend({ + contract: { family: 'app', major: 1 }, + pluginKind: 'crons', + base: '/crons', + routes: [{ kind: 'route', id: 'calendar', path: '/calendar', module: './routes/calendar.tsx' }], + zones: [ + { kind: 'zone', id: 'next-fires', zone: 'app.dashboard.panels', module: './components/NextFiresCard.tsx' }, + ], + requires: { procedures: ['crons.list', 'crons.nextFires'] }, +}); +``` + +The always-current symbol list is `deno doc jsr:@netscript/plugin-frontend-core@<version>`. +```` + +--- + +## Fragment: `@netscript/fresh` — `./plugins` row and section + +Add to the entrypoint table: + +````markdown +| `@netscript/fresh/plugins` | `./src/runtime/plugins/mod.ts` | Plugin frontend mounting: `definePluginPage`, `pluginApi`, `PluginZone`, `pluginNavSections`, `mountPluginFrontends`. | +```` + +Add as a section: + +````markdown +## Plugin frontends + +Pass the generated registry to `defineFreshApp` and every installed plugin's pages, islands, zone +components, nav entries, and theme CSS mount themselves — no app edits: + +```ts +import { defineFreshApp } from '@netscript/fresh/server'; +import { frontendRegistry } from './.netscript/generated/frontend.registry.ts'; + +export const app = defineFreshApp<State>({ name: 'my-app', frontend: frontendRegistry }); +``` + +Mounting runs as a dedicated composition phase after the host's own routes, as thin wrappers over +upstream Fresh primitives: `App.mountApp` for routes, `islandSpecifiers` for islands, `app.use` +for the gateway. Plugin route modules are authored with `definePluginPage` (Fresh `PageProps` plus +the injected plugin contexts); islands call home through `pluginApi`, which targets the generated +deny-by-default procedure gateway. `<PluginZone id>` renders a host-published zone, and +`pluginNavSections(registry, { group })` feeds contributed entries into the app's navigation. + +Plugin authors should read the guide *Ship frontend from your plugin*; app authors only need the +one option above. +```` + +--- + +## Fragment: `@netscript/plugin` — contribution-vocabulary bullet + section + +Extend the contribution-vocabulary bullet: + +````markdown +- **A rich contribution vocabulary** — declare services, background processors, stream topics, + database schemas, migrations, runtime-config topics, telemetry, and **frontend surfaces** as + typed contribution axes. +```` + +Add as a section: + +````markdown +## The frontend axis is a pointer + +A plugin that ships a frontend registers it with one builder call: + +```ts +.withFrontend({ export: './frontend', framework: 'fresh', contract: [{ family: 'app', major: 1 }] }) +``` + +and the matching `scaffold.plugin.json` block: + +```jsonc +{ "frontend": { "export": "./frontend", "framework": "fresh", "contract": [{ "family": "app", "major": 1 }] } } +``` + +The block is a parse-only pointer — the same idiom as `scaffolder.export`. No frontend vocabulary +lives in this package: the typed contract (`defineFrontend`, the five contribution kinds, host +contexts) is owned by `@netscript/plugin-frontend-core`, and discovery is the CLI's generated +registry. Older toolchains ignore the block safely, because an older host also lacks the frontend +generation step — there is no half-wired state. +```` + +--- + +## Fragment: `@netscript/cli` — plugin-frontend verbs section + +````markdown +## Plugin frontend commands + +The CLI discovers, generates, and maintains plugin frontend wiring. The app-side emissions live in +`.netscript/generated/frontend.*` and are produced transactionally — staged, type-checked, and +swapped atomically — so a failed generation never leaves a half-updated host. + +| Command | What it does | +| --- | --- | +| `netscript plugin new <name> --with frontend` | Scaffold the `frontend/` tree, register the manifest axis, and seed the `deno.json` export map. | +| `netscript plugin dev` | In a plugin directory: watch `frontend/`, keep the export map in sync, regenerate the host registry atomically, and signal the app's Vite server to reload. | +| `netscript plugin install <spec>` | Regenerate the frontend emissions and type-check the staged set — a broken module export fails the install with the real diagnostic. | +| `netscript plugin update` / `remove` | Re-pin (or deterministically empty) the emissions; removal can never leave dangling imports. | +| `netscript generate plugins` | Regenerate every axis registry, including `frontend.*`. | +| `netscript generate frontend` | In a plugin directory: derive contribution lists from the file-tree convention and repair the export map. | +| `netscript generate frontend-wiring` | Idempotently adopt the layer in an existing app: Vite island feed, router merge, zone hints, CSS import. | +| `netscript plugin resource add <plugin> <resource> --app <path>` | Copy a scaffolded-starter resource into a chosen app — app-owned from that moment, drift-reported on update. | +| `netscript plugin doctor` | Frontend check: envelope/window handshake, zone validity against the host descriptor, export-map presence, and orphan/stale generated output. | +```` diff --git a/.llm/runs/plan-frontend-contrib--seed/design/docs-story/reference-fresh-plugins.md b/.llm/runs/plan-frontend-contrib--seed/design/docs-story/reference-fresh-plugins.md new file mode 100644 index 000000000..5bb0a47a8 --- /dev/null +++ b/.llm/runs/plan-frontend-contrib--seed/design/docs-story/reference-fresh-plugins.md @@ -0,0 +1,207 @@ +# `@netscript/fresh/plugins` — API reference + +The runtime half of the frontend contribution layer. `@netscript/plugin-frontend-core` owns the +serializable contract vocabulary; this subpath owns everything that touches the framework — +mounting, page helpers, zone rendering, the nav feed, and the route-module normalizer — as thin +wrappers over upstream Fresh primitives. + +The module consumes the **generated registry** the CLI emits into `.netscript/generated/` on +`plugin install` / `netscript generate plugins`: + +| Generated file | Consumed by | +| ----------------------- | ------------------------------------------------------- | +| `frontend.registry.ts` | `defineFreshApp({ frontend })` — identities, resolved bases, contributions, literal lazy route loaders. | +| `frontend.islands.ts` | `vite.config.ts` — `fresh({ islandSpecifiers: pluginIslandSpecifiers })`. | +| `frontend.routes.ts` | `router.ts` — typed hrefs (`routes.plugins.crons.calendar.href()`). | +| `frontend.css` | the app stylesheet — host layer prelude + plugin overlays. | +| `frontend.gateway.ts` | the gateway route table (see below). | +| `frontend.check.ts` | the staged install-time type-check — imports every referenced module. | + +Generation is transactional: the set is staged out-of-place, type-checked, and swapped atomically. +A removed plugin emits the deterministic empty set, so imports can never dangle. + +## `defineFreshApp` — the `frontend` option + +```ts +import { defineFreshApp } from '@netscript/fresh/server'; +import { frontendRegistry } from './.netscript/generated/frontend.registry.ts'; + +export const app = defineFreshApp<State>({ + name: 'my-app', + frontend: frontendRegistry, +}); +``` + +One option mounts everything: plugin sub-apps, nav, zones, the gateway, and host-context +injection. The generated registry exists from day zero — empty when no plugin contributes — so the +wiring is unconditional. + +**Composition order matters.** Fresh compiles each route's middleware and layout chain at +insertion time, so plugin mounting runs as a dedicated phase *after* the host's own routes: + +```text +static files → host middleware → host fs routes/layouts → mountPluginFrontends +``` + +Within each plugin sub-app the order is fixed: **middleware first, then routes**. Contributed +sub-apps may not carry `App`-level `notFound`, `onError`, or `appWrapper` commands; the mount glue +strips them and reports. + +## `mountPluginFrontends` + +```ts +function mountPluginFrontends(app: App, registry: FrontendContributionRegistry): void; +``` + +The composition-phase mount step the `frontend` option invokes. Per plugin, it creates a sub-`App`, +registers the plugin-scope middleware (which injects `state.pluginHost`), registers each route +with its generated literal lazy loader, and mounts the sub-app at the resolved base: + +```ts +// What the phase does, per plugin (sketch): +const sub = new App<State>(); +sub.use(pluginScopeMiddleware(identity)); // BEFORE routes +for (const r of m.routes) sub.route(r.path, r.load); +app.mountApp(m.resolvedBase, sub); +``` + +You never call it directly in a scaffolded app — `defineFreshApp({ frontend })` wires it — but it +is exported for hosts with a custom composition pipeline. + +## `definePluginPage` + +```ts +function definePluginPage(page: (ctx: PluginPageProps) => unknown): unknown; +``` + +Authoring sugar for plugin route modules: a Fresh page whose props carry the injected plugin +contexts. `PluginPageProps` is Fresh `PageProps` plus: + +| Field | Type | Notes | +| -------- | ---------------------- | ------------------------------------------------ | +| `host` | `PluginRequestContext` | Server-only: identity, base, `serviceUrl`, principal port, CSP nonce, abort signal. | +| `client` | `PluginClientContext` | Serializable; the only context an island may receive. | + +```tsx +import { definePluginPage } from '@netscript/fresh/plugins'; +import { createCronsClient } from '@acme/plugin-crons/contracts/v1'; + +export default definePluginPage(async (ctx) => { + const crons = await createCronsClient(ctx.host.serviceUrl('crons-api')).crons.list(); + return <CronCalendar initial={crons} client={ctx.client} />; +}); +``` + +The sugar-free equivalent is plain `define.page`, reading `props.state.pluginHost` yourself. + +## `pluginApi` + +```ts +function pluginApi(client: PluginClientContext): string; +``` + +Returns the gateway base URL for the calling plugin, derived from the client context's `mountId`. +Pass it to a typed client inside an island; the request then travels the generated gateway with +the host's authentication and audit posture. + +```tsx +import { pluginApi } from '@netscript/fresh/plugins'; + +async function refresh() { + entries.value = await createCronsClient(pluginApi(props.client)).crons.list(); +} +``` + +Server-side code does not need `pluginApi` — call typed clients against `ctx.host.serviceUrl(...)` +directly. + +## `PluginZone` + +```tsx +<PluginZone id='app.dashboard.panels' /> +``` + +| Prop | Type | Meaning | +| ---- | -------- | --------------------------------------------------- | +| `id` | `string` | A zone id from the host's `HostSurfaceDescriptor`. | + +Renders the zone's contributions in deterministic order (`order`, then mount id, then contribution +id), each wrapped for scoping and provenance: + +```html +<div data-ns-plugin='crons' data-ns-contribution='crons/next-fires'>…</div> +``` + +**Containment contract.** Data resolution runs host-side before render inside a guard; a failed +resolution renders the quarantine card. Hydrated islands sit under a client-side boundary, so +post-hydration crashes degrade to the card without killing the page. A plugin route that throws +fails that route, host-styled. An SSR **render-time** throw in a zone component fails the page +response — the reason zone components resolve data in the resolver and keep renders pure. + +## `pluginNavSections` + +```ts +function pluginNavSections( + registry: FrontendContributionRegistry, + options: { group: string }, +): SidebarNavSection[]; +``` + +Builds the nav feed for a group from the registry, for `SidebarShell.navigation` or the topbar. +Targets are discriminated: `route` targets get the base path composed and resolve through the +generated typed route refs; `href` renders as-is; `external` renders with +`rel="noopener noreferrer"`. Labels are `MessageRef`s resolved through the host's message +resolution, falling back to the mandatory default text. + +```tsx +// routes/_layout.tsx — contributed entries append to the app's own: +const navigation = [ + ...DESIGN_NAVIGATION, + ...pluginNavSections(frontendRegistry, { group: 'main' }), +]; +``` + +## `normalizeFreshRouteModule` + +```ts +function normalizeFreshRouteModule(module: unknown): Route; +``` + +Maps a file-system route module namespace — `default` (the component), `handler`/`handlers`, +`config`, `css` — onto the internal `Route` shape upstream `App.route()` accepts. Unknown module +members throw a structured error. You never call it yourself; the generated registry applies it in +every literal lazy loader: + +```ts +// .netscript/generated/frontend.registry.ts (excerpt) — AUTO-GENERATED, DO NOT EDIT +{ + id: 'calendar', + path: '/calendar', + load: () => import('@acme/plugin-crons/frontend/routes/calendar').then(normalizeFreshRouteModule), +} +``` + +## The generated gateway + +```ts +// Wired by the `frontend` option via `app.use(...)` — not called directly in a scaffolded app. +pluginGateway(registry); +``` + +`frontend.gateway.ts` is a **deny-by-default route table** generated from each plugin's +`requires.procedures` crossed with its versioned contract metadata — one route per granted +procedure, with the service owner, method, path template, request/response mode, and streaming +policy derived from the contract. Nothing else is reachable; there is no wildcard forwarding. + +The gateway's posture: + +- server-side authentication and authorization through the host principal port — browser + credentials are never blind-forwarded; +- origin/CSRF checks, request/response size limits, timeouts, and abort propagation; +- manual redirect handling and a response-header allowlist; +- a structured audit log line per invocation (mount id, procedure, subject); +- streaming (SSE / event-iterator procedures) only where the contract metadata declares it. + +Islands reach the gateway through `pluginApi`. Durable AI chat streams are **not** gateway +traffic: they stay on the specialized `@netscript/fresh/ai` stream proxy, which the plugin mounts +as its own contributed route. diff --git a/.llm/runs/plan-frontend-contrib--seed/design/docs-story/reference-plugin-frontend-core.md b/.llm/runs/plan-frontend-contrib--seed/design/docs-story/reference-plugin-frontend-core.md new file mode 100644 index 000000000..20f0f5fb5 --- /dev/null +++ b/.llm/runs/plan-frontend-contrib--seed/design/docs-story/reference-plugin-frontend-core.md @@ -0,0 +1,456 @@ +# `@netscript/plugin-frontend-core` — API reference + +The contract package for the frontend contribution layer. It holds **serializable types, schemas, +and the `defineFrontend` validator only** — no Fresh, no Preact, no runtime helpers. Everything +that touches the framework (`definePluginPage`, `pluginApi`, the route-module normalizer, state +injection) lives in `@netscript/fresh/plugins`. Component references are module specifiers — data — +which is what keeps this package framework-free and the registry statically emittable. + +| Entrypoint | What it exports | +| ----------------- | ---------------------------------------------------------------------- | +| `.` | `defineFrontend`, plus a re-export of `./contracts/v1`. | +| `./contracts/v1` | The versioned contract vocabulary documented on this page. | +| `./testing` | The host-fixture test kit every frontend-contributing plugin runs. | + +All schemas validate with Standard Schema and freeze on success. Payload schemas pass through +unknown fields, so adding an optional field to an existing kind is a minor contract change; adding +a kind or a discriminant is a new major of the family. + +## `defineFrontend` + +```ts +function defineFrontend(definition: FrontendDefinition): FrontendManifestEnvelope; +``` + +Validates the keyed authoring form against the envelope and the `app`-family payload schema, then +freezes the result. The default export of a plugin's `./frontend` module is the returned envelope. +A plugin that serves several hosts (the app and the dashboard) exports one envelope per family. + +`FrontendDefinition` is the keyed form; it compiles to the versioned envelope: + +| Field | Type | Required | Meaning | +| ------------ | ----------------------------------------------- | -------- | ---------------------------------------- | +| `contract` | `{ family: 'app'; major: 1 } \| FamilyRef` | yes | Family + major — the host handshake. | +| `pluginKind` | `string` | yes | Canonical plugin kind (`'crons'`). Must match the installer manifest. | +| `base` | `string` | no | Preferred mount base (`'/crons'`); the host may remap. | +| `routes` | `readonly RouteContribution[]` | no | Route contributions. | +| `islands` | `readonly IslandContribution[]` | no | Island contributions. | +| `zones` | `readonly ZoneContribution[]` | no | Zone contributions. | +| `theme` | `readonly ThemeContribution[]` | no | Theme contributions. | +| `requires` | `FrontendRequires` | no | Port and procedure grants. | + +```ts +import { defineFrontend } from '@netscript/plugin-frontend-core'; + +export default defineFrontend({ + contract: { family: 'app', major: 1 }, + pluginKind: 'crons', + base: '/crons', + routes: [{ kind: 'route', id: 'calendar', path: '/calendar', module: './routes/calendar.tsx' }], + requires: { procedures: ['crons.list'] }, +}); +``` + +## `FrontendManifestEnvelope` + +```ts +interface FrontendManifestEnvelope { + readonly contract: { readonly family: 'app'; readonly major: 1 } | FamilyRef; + readonly pluginKind: string; + readonly base?: string; + readonly contributions: readonly unknown[]; + readonly requires?: FrontendRequires; +} +``` + +The stable, versioned shape every host validates. The envelope is validated by the envelope +schema; `contributions` is validated by the **family's** registered payload schema — never by the +envelope — which is what lets new families (the dashboard's, for example) share discovery, +identity, and negotiation without widening this schema. + +**Evolution rules.** New optional field on an existing kind: minor — payload schemas are +passthrough at the boundary. New kind or new discriminant: a new major of that family. Hosts +declare supported `(family, major)` windows in their `HostSurfaceDescriptor` and quarantine +contributions outside the window. + +```ts +// What the keyed crons declaration compiles to (abridged): +{ + contract: { family: 'app', major: 1 }, + pluginKind: 'crons', + base: '/crons', + contributions: [ + { kind: 'route', id: 'calendar', path: '/calendar', module: './routes/calendar.tsx' }, + ], + requires: { procedures: ['crons.list'] }, +} +``` + +## `FamilyRef` + +```ts +interface FamilyRef { + readonly family: string; + readonly major: number; +} +``` + +Identifies a contribution family and major version. The `app` family (major 1) is this package's +payload; other families register their own payload schemas with the hosts that support them. + +```ts +const appV1: FamilyRef = { family: 'app', major: 1 }; +``` + +## The `app` family payload — shared base + +### `AppContributionBase` + +```ts +interface AppContributionBase { + readonly id: string; + readonly title?: MessageRef; + readonly icon?: string; +} +``` + +`id` is unique within `(plugin, family)` — duplicates are generate-time errors. `title` is a +host-localizable message reference; `icon` is a fresh-ui icon name. + +### `MessageRef` + +```ts +interface MessageRef { + readonly id: string; + readonly default: string; +} +``` + +A human-readable string as data: the `id` keys into the host's message catalog, the `default` text +is mandatory so every host renders something. Resolved through the host's message resolution. + +```ts +const label: MessageRef = { id: 'crons.nav.calendar', default: 'Cron calendar' }; +``` + +### `ComponentRef` + +```ts +type ComponentRef = `./${string}`; +``` + +A package-relative module specifier. The generated registry turns each ref into a literal lazy +loader, and the staged type-check imports every referenced module — a ref that is absent from the +package export map is a generate-time error. + +## The five kinds + +### `RouteContribution` + +```ts +interface RouteContribution extends AppContributionBase { + readonly kind: 'route'; + readonly path: string; + readonly module: ComponentRef; + readonly nav?: NavSpec; +} +``` + +A Fresh route module mounted at `base + path` (Fresh pattern syntax: `/schedules/:id`). The +registry emits a literal lazy loader per route, normalized by `normalizeFreshRouteModule` at load +time. Plugin `_layout` modules are not supported and are rejected at generate time. `nav` is +shorthand for a nav contribution targeting this route. + +```ts +const calendar: RouteContribution = { + kind: 'route', + id: 'calendar', + path: '/calendar', + module: './routes/calendar.tsx', + nav: { label: { id: 'crons.nav.calendar', default: 'Cron calendar' }, icon: 'calendar', group: 'main' }, +}; +``` + +### `NavSpec` + +The inline nav declaration on a route: `label` (a `MessageRef`), plus optional `icon` and `group`. +It compiles to a `NavContribution` whose target is `{ kind: 'route', routeId }` of the declaring +route — the base path is composed for you. + +### `IslandContribution` + +```ts +interface IslandContribution extends AppContributionBase { + readonly kind: 'island'; + readonly module: ComponentRef; +} +``` + +One island component per file, registered by module specifier on both the Vite and dev-builder +paths. Props cross Fresh's serialization boundary: **serializable data only** — pass +`PluginClientContext`, never `PluginRequestContext`. The testing kit fails contributions whose +island props do not round-trip. + +```ts +const calendarIsland: IslandContribution = { + kind: 'island', + id: 'cron-calendar', + module: './islands/CronCalendar.tsx', +}; +``` + +### `ZoneContribution` + +```ts +interface ZoneContribution extends AppContributionBase { + readonly kind: 'zone'; + readonly zone: string; + readonly module: ComponentRef; + readonly order?: number; +} +``` + +An SSR component injected into a host-published zone; it may import the plugin's islands. `zone` +is validated against the **target host's** `HostSurfaceDescriptor` — an unknown id is a +generate-time error, a known-but-unmounted zone is skipped with an informational diagnostic. +`order` sorts within the zone (ties break by mount id, then contribution id). + +```ts +const nextFires: ZoneContribution = { + kind: 'zone', + id: 'next-fires', + zone: 'app.dashboard.panels', + module: './components/NextFiresCard.tsx', + order: 10, +}; +``` + +### `NavContribution` + +```ts +interface NavContribution extends AppContributionBase { + readonly kind: 'nav'; + readonly label: MessageRef; + readonly target: + | { readonly kind: 'route'; readonly routeId: string } + | { readonly kind: 'href'; readonly href: string } + | { readonly kind: 'external'; readonly href: string }; + readonly group?: string; + readonly order?: number; +} +``` + +A navigation entry with a discriminated target. `route` targets a route id from the same plugin +(base path composed); `href` is a host-internal absolute path; `external` renders with +`rel="noopener noreferrer"` enforced by the host. `group` is validated against the host's nav +groups. + +```ts +const docsLink: NavContribution = { + kind: 'nav', + id: 'docs', + label: { id: 'crons.nav.docs', default: 'Cron documentation' }, + target: { kind: 'external', href: 'https://example.com/crons' }, + group: 'main', + order: 40, +}; +``` + +### `ThemeContribution` + +```ts +interface ThemeContribution extends AppContributionBase { + readonly kind: 'theme'; + readonly css: readonly ComponentRef[]; +} +``` + +CSS overlays in the `--ns-*` token vocabulary only. Files are aggregated into the generated +stylesheet under the host-owned layer order and scoped by `[data-ns-plugin='<mountId>']` wrappers. + +```ts +const theme: ThemeContribution = { kind: 'theme', id: 'theme', css: ['./theme.css'] }; +``` + +### `AppContribution` + +```ts +type AppContribution = + | RouteContribution + | IslandContribution + | ZoneContribution + | NavContribution + | ThemeContribution; +``` + +The `app`-family payload union, discriminated on `kind`. + +## `FrontendIdentity` + +```ts +interface FrontendIdentity { + readonly packageName: string; + readonly pluginKind: string; + readonly installationId: string; + readonly mountId: string; +} +``` + +Four identities, each with one job — one string cannot serve provenance, URLs, CSS scoping, and +authorization: + +| Field | Assigned by | Job | +| ---------------- | -------------------- | ---------------------------------------------------------- | +| `packageName` | the plugin | Immutable package/source identity (`'@acme/plugin-crons'`); provenance and version drift. | +| `pluginKind` | the plugin | Canonical capability kind (`'crons'`); stable across forks and aliases. | +| `installationId` | the host, at install | Instance id; equals `pluginKind` unless multi-instance. | +| `mountId` | the host, at install | **The** key for routes, CSS scope, gateway paths, and registry entries. | + +All generated keys — base path, `data-ns-plugin` scope, gateway prefix, typed route-ref namespace — +derive from `mountId`, never from `packageName`. + +## `HostSurfaceDescriptor` + +```ts +interface HostSurfaceDescriptor { + readonly host: string; + readonly families: readonly FamilyRef[]; + readonly zones: readonly { readonly id: string; readonly capacity?: number }[]; + readonly navGroups: readonly string[]; + readonly reservedPaths: readonly string[]; +} +``` + +The versioned surface a host publishes. Zones and nav groups are **host data, not schema +constants** — adding a zone to a host is a data change, which is what makes zone growth genuinely +additive. The registry validates contributions against the descriptor of the host it is generated +for; `reservedPaths` (such as `/_fresh` and the gateway prefix) participates in base-collision +checks. + +```ts +// The scaffolded app's v1 descriptor (abridged): +{ + host: 'app', + families: [{ family: 'app', major: 1 }], + zones: [ + { id: 'app.topbar.end' }, + { id: 'app.dashboard.panels' }, + { id: 'app.home.cards' }, + { id: 'app.footer' }, + ], + navGroups: ['main'], + reservedPaths: ['/_fresh', '/api/plugins'], +} +``` + +## `PluginRequestContext` + +```ts +interface PluginRequestContext { + readonly identity: FrontendIdentity; + readonly base: string; + readonly serviceUrl: (service: string) => string; + readonly principal: PrincipalPort | null; + readonly csp: { readonly nonce?: string }; + readonly signal: AbortSignal; +} +``` + +**Server-only.** Injected under Fresh app state (`state.pluginHost`) by the mount middleware and +exposed to pages through `definePluginPage`. It may hold functions and ports — it must never cross +into an island. `serviceUrl` resolves a plugin service through the host's runtime configuration; +`principal` is supplied by the auth plugin when installed and is `null` on unauthenticated hosts; +`csp.nonce` passes through the Fresh CSP seam. + +```ts +// Inside a definePluginPage handler: +const client = createCronsClient(ctx.host.serviceUrl('crons-api')); +if (!ctx.host.principal) { /* render the signed-out state */ } +``` + +## `PrincipalPort` + +The authorization port shape owned by the base contract. The auth plugin supplies the +implementation; unauthenticated hosts carry `principal: null` and take no auth dependency. No +concrete claims type (such as session claims) appears in the base contract — those live with the +auth family. + +## `PluginClientContext` + +```ts +interface PluginClientContext { + readonly mountId: string; + readonly base: string; + readonly locale: string; + readonly direction: 'ltr' | 'rtl'; + readonly timeZone: string; + readonly subject: { readonly signedIn: boolean; readonly display?: string } | null; + readonly capabilities: readonly string[]; +} +``` + +**Serializable** — the one context that may cross into islands, versioned with the family schema. +`capabilities` lists the granted `requires`, so a plugin degrades its UI cleanly when a host does +not provide something. + +```tsx +// A zone component forwarding only the island-safe context: +<CronCalendar initial={entries} client={client} /> +``` + +## `ZoneProps` + +```ts +interface ZoneProps { + readonly url: URL; + readonly host: PluginRequestContext; + readonly client: PluginClientContext; +} +``` + +The props a zone component receives server-side. Forward `client` — or slices of it — to islands; +never `host`. + +```tsx +export default async function NextFiresCard({ host, client }: ZoneProps) { + const fires = await createCronsClient(host.serviceUrl('crons-api')).crons.nextFires(); + return <NextFiresList initial={fires} client={client} />; +} +``` + +## `FrontendRequires` + +```ts +interface FrontendRequires { + readonly ports?: readonly string[]; + readonly procedures?: readonly string[]; +} +``` + +The gateway's input, not prose. Each `procedures` entry names a procedure the plugin's **own** +contract exposes; the generated deny-by-default gateway derives the service owner, method, path +template, and streaming policy from the plugin's versioned contract metadata. Granted entries +surface on `PluginClientContext.capabilities`. + +```ts +requires: { procedures: ['crons.list', 'crons.nextFires'] } +``` + +## `./testing` — the host-fixture test kit + +A generated host fixture that runs the suite every frontend-contributing plugin ships green: + +- schema and resolution checks against the contract and a host surface descriptor; +- island-props serialization round-trip; +- SSR render and hydration; +- browser smoke and accessibility (axe + keyboard); +- base-path composition; +- both delivery modes: local-source and JSR-installed. + +Per-plugin **production budgets** — initial JS bytes, async chunk count, CSS bytes, island count, +zone SSR render deadline, data resolver deadline — are recorded in the plugin manifest and +asserted by the kit; exceptions are explicit host policy, not silent convention. + +Import it from `@netscript/plugin-frontend-core/testing` in the plugin's test suite; the generated +fixture mounts the plugin's envelope against a synthetic host so failures name the contribution, +not a stack frame. diff --git a/.llm/runs/plan-frontend-contrib--seed/drift.md b/.llm/runs/plan-frontend-contrib--seed/drift.md index cb04670a8..f3df064cc 100644 --- a/.llm/runs/plan-frontend-contrib--seed/drift.md +++ b/.llm/runs/plan-frontend-contrib--seed/drift.md @@ -8,3 +8,4 @@ Append-only. Severity: `minor` | `significant` | `architectural`. | 2 | 2026-07-18 | minor | Kickoff replaces seed-run stages F/G/H with a custom pipeline (Codex GPT-5.6 Sol high adversarial → Kimi K3 docs pass), all supervisor-dispatched. This session stops at `STAGE-COMPLETE: generator`; no self-arranged evals. | Recorded in `supervisor.md`; conforms to lane-policy invariants (generator ≠ evaluator). | | 3 | 2026-07-18 | minor | Stage-B discovery ran on Claude Opus 4.8 Explore sub-agents (4-way fan-out) instead of Tier-C committed workflows. Rationale: `subagent-model-routing` standing rule (Fable prohibited for swarm use; Opus is the swarm lane) and CLAUDE.md's "workflows are an expensive supervisor accelerator" caveat. Findings are integrated and re-cited in `research.md`; load-bearing claims were spot-verified by the supervisor session (Fresh 2.3.3 `App.mountApp`/`Builder.registerIsland` verified directly via `deno doc`/jsr source). | Recorded here; citations in `research.md` are the audit trail. | | 4 | 2026-07-19 | minor | Stage-2 adversarial pass dispatched at **Sol · high** per the kickoff's explicit lane spec, not the generic `review_claude` default (Sol · xhigh) in `lane-policy.md`. Owner authorized dispatch in-turn ("proceed and dispatch to codex first"). Launch via `launch-codex-slice.ts` on this worktree (Linux-local); thread `019f7883-ccf0-7820-aa36-3bd90b82ac05`; brief committed at `briefs/adversarial-sol-brief.md`. Worktree's inherited `origin/main` upstream unset to satisfy launcher push-safety (explicit-refspec pushes only). | Kickoff is the owner directive of record; recorded here + `supervisor.md`. | +| 5 | 2026-07-19 | minor | Stage-3 docs pass dispatched on **OpenCode · OpenRouter · `moonshotai/kimi-k3` · high** — the kickoff names Kimi K3 explicitly; the configured OpenCode constant (`OPENCODE_MODEL_IDS.visionEval` = kimi-k2.6) is the vision-eval lane, so the model is passed as an explicit per-run override. Kimi K3 is an open model; owner authorized the pipeline in-turn. Brief at `briefs/kimi-docs-brief.md`. | Recorded here; deliverables land under `design/docs-story/`. | From f39d27a06fa48b947cbac2205ee5cca245902cdc Mon Sep 17 00:00:00 2001 From: Rickylabs <eric.chautems@gmail.com> Date: Sun, 19 Jul 2026 07:02:56 +0200 Subject: [PATCH 07/11] =?UTF-8?q?plan(frontend-contrib):=20integrate=20Kim?= =?UTF-8?q?i=20K3=20docs-story=20notes=20=E2=80=94=20rev=203=20(all=2017?= =?UTF-8?q?=20accepted)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FrontendDefinition named + contract default + MessageRef string shorthand + theme singular; PluginPageContext + redirect; pluginApi(client) and GATEWAY_PREFIX pinned; pointer de-duplicated; ModuleRef rename; budgets + defineFrontendTestSuite; route-param cross-check; doctor taxonomy as product surface; examples re-aligned to rev-2 shapes. Dispositions in docs-story-triage.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015fc7SaneZJ9mzrAhRW4tma --- .../context-pack.md | 12 +- .../design/canonical/00-overview.md | 4 +- .../design/canonical/01-contracts.md | 110 +++++++++++++++--- .../design/canonical/02-authoring-dx.md | 12 +- .../canonical/03-discovery-and-registry.md | 6 +- .../design/canonical/04-host-runtime.md | 9 +- .../canonical/05-scaffolding-and-cli.md | 11 +- .../design/examples/ai.md | 4 +- .../design/examples/dashboard.md | 8 +- .../docs-story-triage.md | 31 +++++ .../plan-frontend-contrib--seed/worklog.md | 22 ++++ 11 files changed, 190 insertions(+), 39 deletions(-) create mode 100644 .llm/runs/plan-frontend-contrib--seed/docs-story-triage.md diff --git a/.llm/runs/plan-frontend-contrib--seed/context-pack.md b/.llm/runs/plan-frontend-contrib--seed/context-pack.md index 731efeac8..bc2de5aa2 100644 --- a/.llm/runs/plan-frontend-contrib--seed/context-pack.md +++ b/.llm/runs/plan-frontend-contrib--seed/context-pack.md @@ -10,7 +10,17 @@ plugin. Drafts only: no PRs/issues, no product code. Custom pipeline: this gener GPT-5.6 Sol high adversarial pass → generator integrates → Kimi K3 public docs/API pass. All downstream stages are supervisor-dispatched. -## State: STAGE 2 (ADVERSARIAL) INTEGRATED — awaiting Kimi K3 docs pass +## State: PIPELINE COMPLETE (rev 3) — awaiting owner review + +All three kickoff stages done: generator → Sol high adversarial (20/20 findings integrated, +rev 2) → Kimi K3 docs story (`design/docs-story/`, 17/17 K-notes integrated, rev 3: +FrontendDefinition, MessageRef shorthand, contract default, ModuleRef, PluginPageContext + +redirect, pinned pluginApi/GATEWAY_PREFIX, budgets + defineFrontendTestSuite, pointer +de-duplication, doctor taxonomy). Triage records: `adversarial-triage.md`, +`docs-story-triage.md`. Next: owner reviews plan.md forks (F1/F2/F3/F5/F7/F8/F9); board filing +is a separate owner-gated step (nothing filed by this run). + +## Stage 2 state (superseded) Sol high review (thread `019f7883-ccf0-7820-aa36-3bd90b82ac05`) produced 20 findings (`adversarial-sol.md`); all accepted and integrated as **rev 2** of plan + canonical docs + diff --git a/.llm/runs/plan-frontend-contrib--seed/design/canonical/00-overview.md b/.llm/runs/plan-frontend-contrib--seed/design/canonical/00-overview.md index 4698108f8..168b260b8 100644 --- a/.llm/runs/plan-frontend-contrib--seed/design/canonical/00-overview.md +++ b/.llm/runs/plan-frontend-contrib--seed/design/canonical/00-overview.md @@ -95,8 +95,8 @@ app/.netscript/generated/ │ consumed by ▼ -app/main.ts defineFreshApp({ name, frontend: fromGenerated(registry) }) ← mounts sub-apps, - nav, zones, proxy +app/main.ts defineFreshApp({ name, frontend: frontendRegistry }) ← mounts sub-apps, + nav, zones, gateway app/vite.config.ts fresh({ islandSpecifiers: pluginIslandSpecifiers }) ← island builds app/routes/… <PluginZone id='app.dashboard.panels' /> ← zone rendering ``` diff --git a/.llm/runs/plan-frontend-contrib--seed/design/canonical/01-contracts.md b/.llm/runs/plan-frontend-contrib--seed/design/canonical/01-contracts.md index f2afc0be2..007ecdb9f 100644 --- a/.llm/runs/plan-frontend-contrib--seed/design/canonical/01-contracts.md +++ b/.llm/runs/plan-frontend-contrib--seed/design/canonical/01-contracts.md @@ -1,7 +1,8 @@ -# Contracts — `@netscript/plugin-frontend-core/contracts/v1` (draft, rev 2) +# Contracts — `@netscript/plugin-frontend-core/contracts/v1` (draft, rev 3) -> **Draft — design document only.** Rev 2 integrates adversarial findings S-2, S-5, S-7, S-8, -> S-9, S-10, S-17 (`../../adversarial-sol.md`, dispositions in `../../adversarial-triage.md`). +> **Draft — design document only.** Rev 2 integrated adversarial findings S-2, S-5, S-7, S-8, +> S-9, S-10, S-17 (`../../adversarial-sol.md`); rev 3 integrates docs-story notes K-4 through +> K-16 (`../docs-story/docs-story-notes.md`, dispositions in `../../docs-story-triage.md`). > Idiom follows `BasePluginContract`: typed seams, Standard-Schema (zod) validation, re-exported — > never redefined — by consumers. @@ -53,6 +54,11 @@ Manifests declare `pluginKind`; the installer/registry assigns `installationId`/ validates `packageName` against the owning plugin. All generated keys (base path, `data-ns-plugin` scope, gateway prefix, typed route ref namespace) derive from `mountId`, never from `packageName`. +**Gateway prefix (pinned — K-13):** the generated gateway for a plugin is mounted at +`/api/plugins/<mountId>/` — the single string that `pluginApi()`, the host descriptor's +`reservedPaths` (`/api/plugins`), and the generated gateway route table all derive from. It is a +contract constant (`GATEWAY_PREFIX`), not a convention. + ## The envelope — versioning that can actually evolve (S-7) A discriminated-union member added to a strict schema is **not** additive: old validators reject @@ -85,8 +91,27 @@ export interface FamilyRef { readonly family: string; readonly major: number } in their `HostSurfaceDescriptor` and quarantine outside the window. Old-host/new-plugin and new-host/old-plugin negotiation each get a contract test. -A plugin's `./frontend` export is one envelope (or an array of envelopes, one per family — e.g. a -plugin contributing to both the app surface and the dashboard). +The envelope also carries the plugin's declared **budgets** (K-9) — asserted by the test kit, +surfaced by the doctor: + +```ts +export interface FrontendBudgets { + readonly initialJsKb?: number; + readonly asyncChunks?: number; + readonly cssKb?: number; + readonly islands?: number; + readonly zoneRenderMs?: number; + readonly resolverMs?: number; +} +// FrontendManifestEnvelope gains: readonly budgets?: FrontendBudgets; +``` + +**Multi-family export form (pinned — K-16):** a plugin's `./frontend` export is one envelope or +a plain array of envelopes, one `defineFrontend` call each: + +```ts +export default [defineFrontend(appDefinition), defineFrontend(dashboardDefinition)]; +``` ## `app` family payload — the five kinds @@ -94,7 +119,12 @@ Shared base: ```ts export interface AppContributionBase { - readonly id: string; // unique within (plugin, family) + /** + * Unique within (plugin, family). The id's jobs (K-12): registry identity, duplicate + * rejection, doctor diagnostics, budget attribution, and — for routes — nav targets and + * typed route-ref names. + */ + readonly id: string; /** Human title as a message ref: host-localizable, default text mandatory (S-17). */ readonly title?: MessageRef; readonly icon?: string; // fresh-ui IconName @@ -102,6 +132,10 @@ export interface AppContributionBase { export interface MessageRef { readonly id: string; readonly default: string } ``` +In the **authoring form** (below), every `MessageRef` position also accepts a plain string +(K-8), compiled to a `MessageRef` with a derived id (`<pluginKind>.<kind>.<contributionId>`) — +the object form is for plugins that ship catalogs. + ### 1. `route` ```ts @@ -117,9 +151,16 @@ export interface RouteContribution extends AppContributionBase { * by @netscript/fresh/plugins and maps default/handler(s)/config/css onto the Route shape. * Plugin `_layout` modules are NOT supported in v1 (explicitly rejected at generate time). */ - readonly module: ComponentRef; + readonly module: ModuleRef; readonly nav?: NavSpec; // convenience: emits a nav contribution targeting this route } +/** Pinned (K-5). label accepts the string shorthand in the authoring form. */ +export interface NavSpec { + readonly label: MessageRef; + readonly icon?: string; + readonly group?: string; + readonly order?: number; +} ``` ### 2. `island` @@ -127,7 +168,7 @@ export interface RouteContribution extends AppContributionBase { ```ts export interface IslandContribution extends AppContributionBase { readonly kind: 'island'; - readonly module: ComponentRef; // one island component per file (Fresh convention) + readonly module: ModuleRef; // one island component per file (Fresh convention) } ``` @@ -142,7 +183,7 @@ host data is island-safe. Build-behavior proof status: Wave-0 gate P3 (`../../pl export interface ZoneContribution extends AppContributionBase { readonly kind: 'zone'; readonly zone: string; // validated against the HOST's published surface (below) - readonly module: ComponentRef; // SSR component; may import the plugin's islands + readonly module: ModuleRef; // SSR component; may import the plugin's islands readonly order?: number; } ``` @@ -167,12 +208,15 @@ export interface NavContribution extends AppContributionBase { ```ts export interface ThemeContribution extends AppContributionBase { readonly kind: 'theme'; - readonly css: readonly ComponentRef[]; // --ns-* vocabulary only; scoping rules in 04 §8 + readonly css: readonly ModuleRef[]; // --ns-* vocabulary only; scoping rules in 04 §8 } ``` +**At most one theme contribution per plugin** (K-6): "the plugin's theme overlay" is one thing +with several files, and the authoring form's `theme` key is correspondingly singular. + ```ts -export type ComponentRef = `./${string}`; +export type ModuleRef = `./${string}`; export type AppContribution = | RouteContribution | IslandContribution | ZoneContribution | NavContribution | ThemeContribution; @@ -247,6 +291,36 @@ export interface FrontendRequires { } ``` +## The authoring form — `FrontendDefinition` (K-15, K-5, K-6, K-7, K-8) + +`defineFrontend(definition: FrontendDefinition): FrontendManifestEnvelope` — the keyed input +authors write, distinct from (and compiled to) the envelope: + +```ts +export interface FrontendDefinition { + /** Defaults to { family: 'app', major: 1 } (K-7); override only for other families. */ + readonly contract?: FamilyRef; + readonly pluginKind: string; + readonly base?: string; + readonly routes?: readonly RouteContribution[]; // NavSpec.label accepts string shorthand + readonly islands?: readonly IslandContribution[]; + readonly zones?: readonly ZoneContribution[]; + /** Standalone nav entries (external links, host-page hrefs) — first-class here (K-5). */ + readonly nav?: readonly NavContribution[]; + /** Singular (K-6): the plugin's one theme overlay, holding its css file list. */ + readonly theme?: ThemeContribution; + readonly requires?: FrontendRequires; + readonly budgets?: FrontendBudgets; +} +``` + +Two further exported names the ecosystem needs (K-15): **`FrontendContributionRegistry`** — the +generated registry's type, owned by this package (hosts' `mountPluginFrontends` / +`pluginNavSections` take it) — and **`PluginPageContext`** — the `definePluginPage` callback +context, owned by `@netscript/fresh/plugins` since it extends Fresh's `PageProps` (typed +`host: PluginRequestContext`, `client: PluginClientContext`, and a `redirect(path): Response` +helper — K-4). + ## Validation & cross-checks - `defineFrontend()` validates the envelope + `app` payload schema and freezes (posture of @@ -259,10 +333,12 @@ export interface FrontendRequires { - Quarantine (render-time) is reserved for contract-window mismatch and load failure — never for known-but-unmounted surfaces. -## The pointer — unchanged +## The pointer — pure pointer, contract derived (K-10) -`@netscript/plugin` learns only `FrontendContributionRef = { export, framework: 'fresh', -contract: FamilyRef[] }` (builder `.withFrontend()`, installer-manifest `frontend` block, -parse-only). `PLUGIN_MANIFEST_SCHEMA_VERSION` bumps additively; older CLIs ignore the block — -and because the older *host* also lacks the frontend generate step, ignoring is safe (no -half-wired state). +`@netscript/plugin` learns only `FrontendContributionRef = { export, framework: 'fresh' }` +(builder `.withFrontend()`, installer-manifest `frontend` block, parse-only). The family/major +handshake lives **once**, in the `./frontend` module's envelope; the registry derives it at +generate time, and `plugin doctor`'s frontend check prints both values if a cached/stale copy +ever disagrees — no second source of truth to drift (K-10). +`PLUGIN_MANIFEST_SCHEMA_VERSION` bumps additively; older CLIs ignore the block — and because the +older *host* also lacks the frontend generate step, ignoring is safe (no half-wired state). diff --git a/.llm/runs/plan-frontend-contrib--seed/design/canonical/02-authoring-dx.md b/.llm/runs/plan-frontend-contrib--seed/design/canonical/02-authoring-dx.md index c8584c97a..14cbde9f0 100644 --- a/.llm/runs/plan-frontend-contrib--seed/design/canonical/02-authoring-dx.md +++ b/.llm/runs/plan-frontend-contrib--seed/design/canonical/02-authoring-dx.md @@ -26,7 +26,7 @@ plugins/crons/ import { defineFrontend } from '@netscript/plugin-frontend-core'; export default defineFrontend({ - contract: { family: 'app', major: 1 }, + // contract defaults to { family: 'app', major: 1 } — override only for other families (K-7) pluginKind: 'crons', base: '/crons', routes: [ @@ -35,7 +35,9 @@ export default defineFrontend({ id: 'calendar', path: '/calendar', module: './routes/calendar.tsx', - nav: { label: { id: 'crons.nav.calendar', default: 'Cron calendar' }, icon: 'calendar', group: 'main' }, + // string labels compile to MessageRefs with derived ids (K-8); pass { id, default } to + // control the catalog key + nav: { label: 'Cron calendar', icon: 'calendar', group: 'main' }, }, { kind: 'route', id: 'schedule-detail', path: '/schedules/:id', module: './routes/schedules/[id].tsx' }, ], @@ -43,7 +45,7 @@ export default defineFrontend({ zones: [ { kind: 'zone', id: 'next-fires', zone: 'app.dashboard.panels', module: './components/NextFiresCard.tsx' }, ], - theme: [{ kind: 'theme', id: 'theme', css: ['./theme.css'] }], + theme: { kind: 'theme', id: 'theme', css: ['./theme.css'] }, // one overlay per plugin (K-6) requires: { procedures: ['crons.list', 'crons.nextFires'] }, }); ``` @@ -53,12 +55,12 @@ export default defineFrontend({ ```ts // plugins/crons/src/public/mod.ts -.withFrontend({ export: './frontend', framework: 'fresh', contract: [{ family: 'app', major: 1 }] }) +.withFrontend({ export: './frontend', framework: 'fresh' }) // family/major derived from the module (K-10) ``` ```jsonc // plugins/crons/scaffold.plugin.json -{ "frontend": { "export": "./frontend", "framework": "fresh", "contract": [{ "family": "app", "major": 1 }] } } +{ "frontend": { "export": "./frontend", "framework": "fresh" } } ``` ## Writing the pages: it is just Fresh (with one import that is honest about where it lives) diff --git a/.llm/runs/plan-frontend-contrib--seed/design/canonical/03-discovery-and-registry.md b/.llm/runs/plan-frontend-contrib--seed/design/canonical/03-discovery-and-registry.md index d19fc7f05..47fd3d79d 100644 --- a/.llm/runs/plan-frontend-contrib--seed/design/canonical/03-discovery-and-registry.md +++ b/.llm/runs/plan-frontend-contrib--seed/design/canonical/03-discovery-and-registry.md @@ -70,6 +70,7 @@ Deterministic order everywhere (plugins by mountId, contributions by (`order`, m | duplicate contribution id within (plugin, family) | error | | `(family, major)` outside the host's declared window | **quarantine** entry (fix-CLI render state), never a host crash | | module ref absent from the package export map | error + `plugin dev`/`generate frontend` hint | +| route `path` params ≠ module filename params (`:id` vs `[id].tsx` — K-11) | error naming both syntaxes | ## 4. Install / update / remove lifecycle (S-11) @@ -88,7 +89,10 @@ Deterministic order everywhere (plugins by mountId, contributions by (`order`, m - **`netscript plugin doctor`** — `frontend` check: envelope/window handshake, zone validity vs the host descriptor, export-map presence, **orphan/stale generated-output detection** (a registry entry with no installed plugin, a css import with no file, a starter whose plugin is - gone). + gone). The doctor prints the **five-state diagnosis taxonomy verbatim** (unknown zone / + known-but-unmounted / capacity-rejected / window-mismatch quarantine / load-failure + quarantine), and every quarantine render card deep-links the doctor command — the taxonomy is + product surface, not internal vocabulary (K-17). - Generation is idempotent (byte-identical skip) and **transactional**: stage → check → swap or rollback; a failed generation leaves the previous set untouched. diff --git a/.llm/runs/plan-frontend-contrib--seed/design/canonical/04-host-runtime.md b/.llm/runs/plan-frontend-contrib--seed/design/canonical/04-host-runtime.md index aa5f5c44c..ace20ad13 100644 --- a/.llm/runs/plan-frontend-contrib--seed/design/canonical/04-host-runtime.md +++ b/.llm/runs/plan-frontend-contrib--seed/design/canonical/04-host-runtime.md @@ -100,8 +100,13 @@ port, CSP nonce seam, abort signal) is injected by `pluginScopeMiddleware`; `PluginClientContext` (serializable: mountId, base, locale, direction, timeZone, subject summary, capabilities) is what may cross into islands. Shapes in `01-contracts.md`. Zone components receive both; pages read them from state via the `definePluginPage` helper — which -lives in **`@netscript/fresh/plugins`** (S-5), typed over Fresh `PageProps` with the injected -state; plain `define.page` + `props.state.pluginHost` is the sugar-free equivalent. +lives in **`@netscript/fresh/plugins`** (S-5), typed as **`PluginPageContext`** over Fresh +`PageProps`: `ctx.host`, `ctx.client`, and a `redirect(path): Response` helper (K-4, K-15); +plain `define.page` + `props.state.pluginHost` is the sugar-free equivalent. +**`pluginApi` signature (pinned — K-2):** `pluginApi(client: PluginClientContext): string` — +returns the plugin's gateway base (`/api/plugins/<mountId>`, the `GATEWAY_PREFIX` contract +constant, K-13); islands hold the client context anyway, and a raw-id overload would invite +hand-typed strings. ## 6. Zones — SSR injection with honest containment (S-4) diff --git a/.llm/runs/plan-frontend-contrib--seed/design/canonical/05-scaffolding-and-cli.md b/.llm/runs/plan-frontend-contrib--seed/design/canonical/05-scaffolding-and-cli.md index 6a5e88fd3..c1d92da1a 100644 --- a/.llm/runs/plan-frontend-contrib--seed/design/canonical/05-scaffolding-and-cli.md +++ b/.llm/runs/plan-frontend-contrib--seed/design/canonical/05-scaffolding-and-cli.md @@ -78,11 +78,12 @@ my-app/ Two layers, both specified in `../plan.md` §Gates: -- **Per-plugin test kit** (`@netscript/plugin-frontend-core/testing`): a generated host fixture - that runs schema/resolution checks, island-props serialization round-trip, SSR render, - hydration, browser smoke (Playwright), a11y (axe + keyboard), base-path composition, and - local-source + JSR modes. A plugin frontend ships with this suite green — the analog of the - e2e axis plugins already declare. +- **Per-plugin test kit** (`@netscript/plugin-frontend-core/testing`, entrypoint + **`defineFrontendTestSuite(manifest, options)`** — K-9; `plugin new --with frontend` seeds a + `frontend_test.ts` calling it): a generated host fixture that runs schema/resolution checks, + island-props serialization round-trip, SSR render, hydration, browser smoke (Playwright), + a11y (axe + keyboard), base-path composition, and local-source + JSR modes. A plugin frontend + ships with this suite green — the analog of the e2e axis plugins already declare. - **Budgets**: per-plugin production budgets recorded in the manifest and asserted by the kit — initial JS bytes, async chunk count, CSS bytes, island count, zone SSR render deadline, data resolver deadline. Exceptions are explicit host policy, not silent convention. diff --git a/.llm/runs/plan-frontend-contrib--seed/design/examples/ai.md b/.llm/runs/plan-frontend-contrib--seed/design/examples/ai.md index dbafda6f6..e0dda4c33 100644 --- a/.llm/runs/plan-frontend-contrib--seed/design/examples/ai.md +++ b/.llm/runs/plan-frontend-contrib--seed/design/examples/ai.md @@ -30,8 +30,8 @@ import { defineFrontend } from '@netscript/plugin-frontend-core'; export default defineFrontend({ - contract: 'v1', - plugin: 'ai', + // contract defaults to { family: 'app', major: 1 } + pluginKind: 'ai', base: '/ai', routes: [ // LIVE: a ready-to-use chat console — durable sessions, model picker, tool calls. diff --git a/.llm/runs/plan-frontend-contrib--seed/design/examples/dashboard.md b/.llm/runs/plan-frontend-contrib--seed/design/examples/dashboard.md index afc5fd0f9..a8b2ef35e 100644 --- a/.llm/runs/plan-frontend-contrib--seed/design/examples/dashboard.md +++ b/.llm/runs/plan-frontend-contrib--seed/design/examples/dashboard.md @@ -20,8 +20,8 @@ host needed: ```ts // plugins/workers/frontend/mod.ts export default defineFrontend({ - contract: 'v1', - plugin: 'workers', + // contract defaults to { family: 'app', major: 1 } + pluginKind: 'workers', base: '/workers', routes: [{ kind: 'route', id: 'console', path: '/', module: './routes/console.tsx', @@ -42,12 +42,12 @@ import type { ZoneProps } from '@netscript/plugin-frontend-core/contracts/v1'; import { createWorkersClient } from '@netscript/plugin-workers-core/contracts/v1'; import { QueueLive } from '../islands/QueueLive.tsx'; -export default async function QueueHealthPanel({ host }: ZoneProps) { +export default async function QueueHealthPanel({ host, client }: ZoneProps) { const stats = await createWorkersClient(host.serviceUrl('workers-api')).jobs.stats(); return ( <article class='nsw-panel'>{/* --ns-* styled card */} <h3>Job queues</h3> - <QueueLive initial={stats} /> {/* hydrates; polls via pluginApi('workers') */} + <QueueLive initial={stats} client={client} /> {/* hydrates; polls via pluginApi(client) — K-2 */} <a href={`${host.base}/`}>Open workers console →</a> </article> ); diff --git a/.llm/runs/plan-frontend-contrib--seed/docs-story-triage.md b/.llm/runs/plan-frontend-contrib--seed/docs-story-triage.md new file mode 100644 index 000000000..c8f9cd334 --- /dev/null +++ b/.llm/runs/plan-frontend-contrib--seed/docs-story-triage.md @@ -0,0 +1,31 @@ +# Docs-story Triage — K-note dispositions (generator, stage 3 integration) + +Verdict on the docs pass: **high quality.** Files 1–4 are public-clean (verified: zero +harness/model/proof references), match the rev-2 contracts, and the K-notes do exactly what the +stage exists for — surface API warts by forcing the docs to be written. **All 17 accepted.** + +| # | Note | Disposition → where applied | +| --- | --- | --- | +| K-1 | dashboard/ai examples still spoke rev 1 | examples updated to rev-2 envelope shapes | +| K-2 | `pluginApi` signature stated two ways | locked `pluginApi(client: PluginClientContext): string` (04 §5); dashboard example comment fixed | +| K-3 | `fromGenerated` vs bare registry | bare registry; overview diagram fixed | +| K-4 | `ctx.redirect` uncontracted | `PluginPageContext` named + typed `redirect` helper (01, 04 §5) | +| K-5 | standalone nav authoring path; `NavSpec.order` | `nav?: readonly NavContribution[]` in the authoring form; NavSpec pinned with `order` (01) | +| K-6 | `theme` key asymmetry | `theme` is singular in fact: ONE ThemeContribution, multiple css files (01, 02) | +| K-7 | `contract` boilerplate | defaults to `{ family: 'app', major: 1 }` in `defineFrontend`, overridable (01, 02) | +| K-8 | `MessageRef` too heavy for the common case | string shorthand compiling to MessageRef with derived id (01, 02) | +| K-9 | testing kit unnamed; budgets homeless | `defineFrontendTestSuite(manifest, options)` named; `budgets` block lives on the envelope (01, 05) | +| K-10 | pointer restates the contract triple | pointer carries `{export, framework}` only; contract derived at generate time; doctor prints drift (01) | +| K-11 | two route-param syntaxes | generate-time param cross-check row added (03 §3) + guide sentence noted for docs refresh | +| K-12 | island `id` had no stated job | purpose documented: registry identity, duplicate rejection, doctor, budget attribution (01) | +| K-13 | gateway prefix unpinned | `/api/plugins/<mountId>/…` pinned next to FrontendIdentity (01); 04 §4 references it | +| K-14 | `ComponentRef` misnames CSS refs | renamed `ModuleRef` everywhere (01, 03) | +| K-15 | three types unnamed | `FrontendDefinition`, `PluginPageContext`, `FrontendContributionRegistry` (owner: plugin-frontend-core) named (01, 04) | +| K-16 | multi-family export unshown | pinned: `export default [defineFrontend(a), defineFrontend(b)]` (01) | +| K-17 | quarantine taxonomy invisible in product | doctor prints the five-state taxonomy verbatim; quarantine card deep-links the doctor command (03 §4, 04 §6) | + +**Docs-story refresh policy:** files 1–4 are the pre-K-note forecast (they documented the API +that existed when they were written; the notes themselves list the touch points). They are a +forecast artifact, not the normative contract — the canonical docs are normative. The doc files +get their matching edits when the contracts are frozen at implementation time (Wave 1), so the +forecast is refreshed once, not per integration round. diff --git a/.llm/runs/plan-frontend-contrib--seed/worklog.md b/.llm/runs/plan-frontend-contrib--seed/worklog.md index 462730ecf..a1da220be 100644 --- a/.llm/runs/plan-frontend-contrib--seed/worklog.md +++ b/.llm/runs/plan-frontend-contrib--seed/worklog.md @@ -76,3 +76,25 @@ STAGE-COMPLETE: generator (S-20; F4/F6 resolved, F9 added). STAGE-COMPLETE: adversarial-integration + +## 2026-07-19 — Stage 3: Kimi K3 docs story dispatched + integrated + +- Dispatched on OpenCode · OpenRouter · `moonshotai/kimi-k3` · high (drift #5); brief + `briefs/kimi-docs-brief.md`. Kimi committed `73cbd48b` (local; pushed by supervisor): + `design/docs-story/` — as-if-shipped guide (318 lines), two API references (207+456), + README fragments (148), and `docs-story-notes.md` (17 K-notes + pending-proof ledger). +- Quality: public-clean verified (zero harness/model/proof references in files 1–4); docs match + rev-2 contracts; the K-notes are precisely the doc-driven API critique the stage exists for. +- **All 17 K-notes accepted** (`docs-story-triage.md`) and integrated as **rev 3**: contract + default (K-7), string MessageRef shorthand (K-8), `theme` singular (K-6), `FrontendDefinition` + named + standalone `nav` array + `NavSpec` pinned (K-5/K-15), `PluginPageContext` + + `redirect` (K-4), `pluginApi(client)` pinned (K-2), gateway prefix `GATEWAY_PREFIX` pinned + (K-13), pointer de-duplicated (K-10), `ModuleRef` rename (K-14), island-id purpose (K-12), + route-param cross-check (K-11), budgets on the envelope + `defineFrontendTestSuite` (K-9), + multi-family export pinned (K-16), doctor-taxonomy-as-product (K-17), examples re-aligned + (K-1), overview diagram fixed (K-3). +- Docs-story refresh policy recorded in `docs-story-triage.md`: files 1–4 remain the pre-K-note + forecast; they get one refresh when contracts freeze at Wave 1. + +STAGE-COMPLETE: docs-story-integration — full kickoff pipeline done; awaiting owner review of +plan.md forks (F1/F2/F3/F5/F7/F8/F9) and downstream filing decisions. From 5691c018ea1ee2ba1d9c28008411d01f61d43662 Mon Sep 17 00:00:00 2001 From: Rickylabs <eric.chautems@gmail.com> Date: Sun, 19 Jul 2026 07:53:29 +0200 Subject: [PATCH 08/11] =?UTF-8?q?plan(frontend-contrib):=20RFC=20document?= =?UTF-8?q?=20=E2=80=94=20frontend=20contribution=20layer=20(draft-PR=20de?= =?UTF-8?q?liverable)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015fc7SaneZJ9mzrAhRW4tma --- .llm/runs/plan-frontend-contrib--seed/rfc.md | 286 +++++++++++++++++++ 1 file changed, 286 insertions(+) create mode 100644 .llm/runs/plan-frontend-contrib--seed/rfc.md diff --git a/.llm/runs/plan-frontend-contrib--seed/rfc.md b/.llm/runs/plan-frontend-contrib--seed/rfc.md new file mode 100644 index 000000000..7233d4fc9 --- /dev/null +++ b/.llm/runs/plan-frontend-contrib--seed/rfc.md @@ -0,0 +1,286 @@ +# RFC — NetScript Frontend Contribution Layer: plugins that ship UI + +| | | +| --- | --- | +| **Status** | **Proposed** — full pipeline complete (generator → GPT-5.6 Sol·high adversarial, 20/20 integrated → Kimi K3 docs story, 17/17 integrated → rev 3); awaiting owner ratification of the open forks (§9) | +| **Tracking** | Refs #427 (dashboard panel seam) · #432 (add-to-app scaffold flow) · #400 (dev-dashboard epic, beta.13) · **Epic FCL-EPIC (placeholder — filed in a later supervisor-coordinated step)** · sibling RFC: deploy-plugin lane (`plan/deploy-plugin`) | +| **Run record** | `.llm/runs/plan-frontend-contrib--seed/` — research, plan (D1–D16 / forks), canonical design 00–06, 4 worked examples, docs-story forecast, both review-triage records | +| **Evidence base** | Upstream verified in-source: `App.mountApp` (@fresh/core 2.3.3 app.ts:357), `Builder.registerIsland` (dev/builder.ts:157), `fresh({ islandSpecifiers })` (@fresh/plugin-vite 1.0.8 mod.ts:211-214); repo precedent: axis registry emitter, `scaffolder.export` pointer, fresh-ui copy-registry + `--ns-*` tokens, ai chat-route scaffolder; prior art: `dashboard-design--orchestrator/analysis/plugin-extension-architecture.md` | + +--- + +## Abstract + +Today a NetScript plugin contributes services, workers, schemas, streams, migrations, and Aspire +wiring — but **not one pixel of frontend**. There is no `.tsx` under `plugins/`, the scaffolded +app's nav is a hardcoded array, and four announced features are blocked on the same missing axis: +the **dev dashboard** (plugin panels), **auth** (sign-in, session, org UI), **ai** (chat and +distributed assist surfaces), and the new **deploy plugin** (cloud consoles + cloud-first +scaffold seams). + +This RFC makes a plugin a **full-stack unit**: + +``` +plugins/crons/frontend/ # ordinary Fresh code: routes/, islands/, components/, theme.css + mod.ts # one defineFrontend() declaration — the whole contract +netscript plugin install @acme/plugin-crons + → /crons/calendar serves · nav entry appears · dashboard panel renders · island hydrates + → zero app edits +``` + +Contributions are **data** (schema-first contracts in `@netscript/plugin-frontend-core`), +discovered like every existing axis (manifest pointer + generated, type-checked registry in +`.netscript/generated/`), and mounted through thin wrappers over upstream Fresh 2.3 primitives +that already exist. Two delivery models are first-class: **live** (package-served, versioned — +consoles, panels, widgets) and **scaffolded starter** (app-owned, customizable — sign-in pages, +cloud-first seams). The dev dashboard's ratified 7-kind contribution family becomes a sibling +payload family on the same envelope — one discovery pipeline, many hosts. + +## 1. Motivation + +| Today | Consequence | +| --- | --- | +| No frontend axis in `PluginContributions`; `PluginType 'frontend'` is an inert enum | A "frontend" plugin cannot ship frontend | +| Scaffolded nav/panels are hardcoded arrays in templates | Installing a plugin changes nothing visible; every surface is app-maintained | +| fresh-ui is copy-only; islands are discovered only in the app's `islands/` dir | Plugin UI has no delivery path at all — the ai plugin resorts to scaffolding a stub `chat.tsx` into the app | +| The dashboard extension architecture (beta-10) was designed dashboard-only and never built | Each future host would re-invent discovery, versioning, and trust | + +The blocked consumers are not hypothetical: auth has a complete oRPC surface +(`signin/callback/signout/session/me`) with zero UI; ai has a durable-chat runtime plane +(`@netscript/fresh/ai`) with only a scaffold-time stub; the deploy redirect explicitly demands +"contribute to every layer (even frontend soon)"; the dashboard epic (#400, beta.13) needs the +seam this RFC generalizes. + +## 2. The proposed decision and its rationale + +**Decision (D1–D16, locked at generator level and twice-reviewed):** build one host-agnostic +frontend contribution layer with these load-bearing choices — + +1. **Contract-first, framework-free vocabulary.** New Archetype-1 package + `@netscript/plugin-frontend-core` holds serializable types + schemas only (no fresh/preact + deps); all runtime helpers live in `@netscript/fresh/plugins`. *Rationale:* keeps the + registry statically emittable and the contracts JSR-clean; enforced by the adversarial pass + (S-5). +2. **Envelope + family versioning.** A stable `FrontendManifestEnvelope` carries + `(family, major)`-versioned payloads; the `app` family v1 has five kinds (route / island / + zone / nav / theme); the dashboard family is a sibling payload, not a widened union. + *Rationale:* adding union members breaks strict old validators — the naive "additive union" + was disproved in review (S-7). +3. **Pointer-thin core.** `@netscript/plugin` learns only + `.withFrontend({ export, framework })`; the handshake is derived from the module at generate + time. *Rationale:* resolves the long-open OQ-12 thinness tension with zero core vocabulary + and no dual source of truth (K-10). +4. **Generated registry, never runtime mutation.** `netscript generate plugins` emits a + transactional replace-set (`frontend.registry.ts`, `frontend.islands.ts`, + `frontend.routes.ts`, `frontend.css`, `frontend.gateway.ts`, `frontend.check.ts`) — staged, + type-checked (the check module imports every referenced module), atomically swapped; + deterministic empty emissions make removal clean. *Rationale:* mirrors the shipped per-axis + emitter; the check module gives the install gate real teeth (S-11). +5. **Wrap upstream, don't reinvent.** Routes mount as lazy sub-Apps via `App.mountApp` in a + post-fsRoutes composition phase with generated literal loaders + a route-module normalizer; + islands register by module specifier on both build paths; nav feeds the existing + `SidebarShell` model; theming is `--ns-*` tokens under a host-owned CSS layer order. + *Rationale:* every primitive verified in upstream source; the ordering and normalization + subtleties were review findings (S-1, S-2) and are Wave-0 proofs before contracts freeze. +6. **A deny-by-default procedure gateway, not a proxy.** Client-side data access goes through a + generated route table derived from `requires.procedures` × the plugin's contract metadata — + no wildcard forwarding, server-side auth, no blind credential forwarding, streaming only + where declared. *Rationale:* a generic forwarding proxy is a confused-deputy surface (S-6); + the shipped AI stream proxy shows the safe pattern. +7. **Two delivery models with a rule.** If the user will edit it, scaffold it (starter via a new + `AppTarget` on `ScaffolderContext` — the existing `toScaffold()` engine); if the plugin owns + it, serve it live. Starters never fabricate backend surface (S-13 law). +8. **Honest trust and containment.** App surface is T0 (installed plugins already run server + code); containment = host-side data-phase catch + client boundaries + route `onError`; SSR + render throws are documented page failures, not false "isolation" (S-4). Dashboard T1/T2 + iframe tiers remain the dashboard epic's scope. +9. **DX is the bar.** The authoring surface (one `defineFrontend`, ordinary Fresh files, string + label shorthands, contract defaulting, tooling-owned export maps, `netscript plugin dev`) + was fixed first and defended through both review passes; a docs-forecast stage (Kimi K3) + wrote the public guide + references against the API and its 17 findings were folded back in. + +## 3. Public API design (implementation-level) + +### 3.1 What a plugin author writes + +```ts +// plugins/crons/frontend/mod.ts — the whole declaration +import { defineFrontend } from '@netscript/plugin-frontend-core'; + +export default defineFrontend({ + // contract defaults to { family: 'app', major: 1 } + pluginKind: 'crons', + base: '/crons', + routes: [{ + kind: 'route', id: 'calendar', path: '/calendar', module: './routes/calendar.tsx', + nav: { label: 'Cron calendar', icon: 'calendar', group: 'main' }, // string → MessageRef + }], + islands: [{ kind: 'island', id: 'cron-calendar', module: './islands/CronCalendar.tsx' }], + zones: [{ kind: 'zone', id: 'next-fires', zone: 'app.dashboard.panels', module: './components/NextFiresCard.tsx' }], + theme: { kind: 'theme', id: 'theme', css: ['./theme.css'] }, + requires: { procedures: ['crons.list', 'crons.nextFires'] }, +}); +``` + +```ts +// plugins/crons/src/public/mod.ts — one added builder call +.withFrontend({ export: './frontend', framework: 'fresh' }) +``` + +Pages/islands are ordinary Fresh modules; islands are imported and rendered directly (the build +registers their specifiers); `definePluginPage` (from `@netscript/fresh/plugins`) types +`ctx.host` / `ctx.client` / `ctx.redirect`; `pluginApi(ctx.client)` returns the plugin's gateway +base (`/api/plugins/<mountId>` — a pinned contract constant). + +### 3.2 Contract surface (`@netscript/plugin-frontend-core/contracts/v1`) + +- `FrontendManifestEnvelope` `{ contract: FamilyRef, pluginKind, base?, contributions, + requires?, budgets? }`; multi-family = an array of envelopes. +- `app` family kinds: `RouteContribution` (literal-loader mounted, `_layout` rejected v1), + `IslandContribution` (serializable props law), `ZoneContribution`, `NavContribution` + (discriminated `route`/`href`/`external` targets, MessageRef labels), `ThemeContribution` + (one per plugin, `--ns-*` only). +- **Identity quartet**: `packageName` / `pluginKind` / `installationId` / `mountId` — all + generated keys (base path, CSS scope, gateway prefix, route-ref namespace) derive from + `mountId`. +- **`HostSurfaceDescriptor`**: hosts publish zones, nav groups, reserved paths, and supported + `(family, major)` windows; diagnoses distinguish unknown zone (error) vs known-but-unmounted + (info) vs capacity-rejected vs quarantine (window mismatch / load failure). +- **Context split**: server `PluginRequestContext` (serviceUrl, principal *port*, CSP nonce, + abort) vs serializable `PluginClientContext` (locale/direction/timeZone, subject summary, + capabilities) — the island boundary is typed, not folklore. +- `./testing`: `defineFrontendTestSuite(manifest, options)` — schema, props round-trip, SSR, + hydration, browser smoke, a11y, base-path, local+JSR modes; asserts the envelope's `budgets`. + +### 3.3 Host wiring (scaffolded by default) + +```ts +// app/main.ts +export const app = defineFreshApp<State>({ name: 'my-app', frontend: frontendRegistry }); +// app/vite.config.ts +fresh({ islandSpecifiers: [...pluginIslandSpecifiers] }) +// app markup +<PluginZone id='app.dashboard.panels' /> +``` + +CLI: `plugin install/update/remove` regenerate transactionally; `plugin new --with frontend` +scaffolds the tree + exports; `netscript plugin dev` owns the watch/regenerate/reload loop; +`plugin doctor` prints the five-state diagnosis taxonomy; `plugin resource add <plugin> +<resource> --app <path>` lands app-owned starters; `generate frontend-wiring` adopts existing +apps idempotently. + +### 3.4 Consumers (worked in the run's `design/examples/`) + +| Consumer | v1 shape | +| --- | --- | +| Dev dashboard | workers/sagas/triggers/streams each ship a zone panel + console route into the *scaffolded app's* `/dashboard` now; the dashboard host later consumes the same registry under `/plugins/<mountId>` with its own family | +| Auth | live: `/auth/account` + topbar session widget over the real 5 procedures; starter: sign-in/callback pages; org console is gated on a named `auth-org` backend capability (does not exist yet — no invented APIs) | +| AI | live: durable-session chat route (specialized stream proxy, not the gateway) + assist launcher island behind a capability check; starter chat page remains the eject path | +| Deploy | core console publishes deploy-family zones; each target adapter contributes its own panels (core imports no target); cloud-first starters (`cloudflare-edge` etc.) via AppTarget | + +## 4. Plan — waves and gates + +**Wave 0 — five disposable proofs before any contract freezes** (the adversarial pass's central +demand): P1 mount ordering/layout/basePath · P2 literal loaders + route normalization · P3 +dependency-island matrix (clean cache × local/JSR × dev/prod, Preact identity, CSS, edit loop) · +P4 SSR containment fixtures · P5 gateway threat model + stream abort. + +**Wave 1** — contracts + spine (envelope/schemas, identity, transactional emissions + +`frontend.check.ts`, mount glue, host descriptor, scaffold wiring, workers dogfood). +**Wave 1b** — the gateway (own reviewed wave). **Wave 2** — DX + lifecycle (`plugin new/dev`, +doctor taxonomy, quarantine states, AppTarget starters, test kit + budgets, adoption verb). +**Wave 3** — consumers (auth v1, ai chat, remaining panels; dashboard + deploy epics consume the +layer in their own runs). + +Gates: per-archetype F-* columns + SCOPE-frontend browser gates; `deno task quality:scan` + +`arch:check` on every `packages/**`/`plugins/**` slice; the per-plugin test kit; extended +`scaffold.runtime` (install → render → hydrate → remove) at merge-readiness. + +## 5. Proposed board (placeholders — NOT filed; filing is a later supervisor-coordinated step) + +**Epic FCL-EPIC** — `Epic: Frontend contribution layer` · labels `type:umbrella`, +`epic:frontend-contrib`, `area:fresh`, `area:plugins` · milestone Backlog / Triage (owner +schedules later). Children (`[frontend-contrib S<n>] …`, each `Part of #FCL-EPIC`): + +| ID | Wave | Slice | +| --- | --- | --- | +| FCB-1 | 0 | P1 proof: mounted sub-app ordering, layouts, basePath, two plugins | +| FCB-2 | 0 | P2 proof: literal lazy loaders + `normalizeFreshRouteModule` | +| FCB-3 | 0 | P3 proof: dependency-island build matrix (local/JSR, dev/prod, HMR, CSS, Preact identity) + plugin-vite pin policy | +| FCB-4 | 0 | P4 proof: SSR failure-containment fixtures | +| FCB-5 | 0 | P5 proof: gateway threat model + SSE abort/reconnect on one real procedure | +| FCB-6 | 1 | `@netscript/plugin-frontend-core` contracts/v1 (envelope, families, identity, schemas, `defineFrontend`) | +| FCB-7 | 1 | `@netscript/plugin` pointer axis (`.withFrontend`, manifest block, `CONTRIBUTION_AXES`) | +| FCB-8 | 1 | Registry emissions: transactional replace-set + `frontend.check.ts` + install/update/remove regeneration | +| FCB-9 | 1 | `@netscript/fresh/plugins`: composition phase, mount glue, normalizer, `PluginZone`, nav feed, `definePluginPage`/`pluginApi` | +| FCB-10 | 1 | Scaffold template wiring + `HostSurfaceDescriptor` + vite `islandSpecifiers` feed | +| FCB-11 | 1 | Workers dogfood: zone panel + console route + island | +| FCB-12 | 1b | Generated deny-by-default procedure gateway | +| FCB-13 | 2 | `plugin new --with frontend` (tree, manifest, seeded exports, `frontend_test.ts`) | +| FCB-14 | 2 | `netscript plugin dev` (watch, export-map maintenance, atomic regen, reload signal) | +| FCB-15 | 2 | Doctor `frontend` check + five-state taxonomy + orphan detection | +| FCB-16 | 2 | Quarantine render states + provenance chrome | +| FCB-17 | 2 | `AppTarget` on `ScaffolderContext` + `plugin resource add --app` | +| FCB-18 | 2 | `./testing` kit (`defineFrontendTestSuite`) + budgets enforcement | +| FCB-19 | 2 | `generate frontend-wiring` adoption verb for existing apps | +| FCB-20 | 3 | Auth v1 frontend (account route, session widget, starter signin/callback) | +| FCB-21 | 3 | AI frontend (durable chat route + specialized proxy, assist launcher) | +| FCB-22 | 3 | Sagas/triggers/streams dashboard-zone panels | +| FCB-23 | 3 | `auth-org` backend capability (prerequisite for the org console — contract + adapters) | +| FCB-24 | 3 | Convention generator (`generate frontend`) — file-tree → manifest lists | + +## 6. Migration / supersession map (proposed dispositions — no closes in this RFC) + +| Item | Disposition | +| --- | --- | +| #427 `DashboardPanelContribution` seam (DDX-17) | **KEEP, re-baseline**: the dashboard family becomes a sibling payload on this RFC's envelope; discovery/registry/doctor arrive from FCB-6/8 — the dashboard epic implements kinds + host, not pipeline | +| #432 add-to-app scaffold flow (DDX-19) | **KEEP, re-baseline**: its engine is FCB-17 (`AppTarget`); the dashboard "second caller" story is unchanged | +| Dev-dashboard epic #400 (beta.13) | **CONSUMER**: starts on this layer's pipeline + four dogfood panels instead of inventing discovery | +| Deploy lane (`plan/deploy-plugin`, sibling RFC) | **CONSUMER**: its frontend row = `defineFrontend` consoles + AppTarget cloud starters; adapter panels dogfood the zone mechanism | +| ai `chat-route` scaffolder | **REPOSITIONED**: remains as the starter/eject path; the live chat route (FCB-21) becomes the default | +| OQ-12 (core axis vs contract seam, roadmap-expansion A-dashboard) | **RESOLVED** by the pointer-axis decision (§2.3) | +| Prior-run arch-debt `generated-ts-frontend-react-reference` | Expected fix-adjacent in Wave 1 template work; verified by the extended `scaffold.runtime` gate | +| Existing apps (already scaffolded) | Non-breaking: layer arrives via `generate frontend-wiring` (idempotent opt-in); new scaffolds get it by default | + +## 7. Security model (summary) + +T0 trust on the app surface (installed plugins already execute server-side); the gateway is the +only client-reachable data path — deny-by-default generated routes, server-side auth via the +principal port, no blind credential forwarding, CSRF/origin checks, limits/timeouts/abort, +manual redirects, response-header allowlist, per-invocation audit line; `requires` is the +auditable grant surface; CSS confined by layer order + `[data-ns-plugin]` scoping + per-plugin +portal roots; quarantine-not-crash rendering for contract drift; T1/T2 sandbox tiers deferred to +the dashboard epic by design. + +## 8. Review trail + +Three-stage pipeline, generator ≠ reviewer sessions throughout: **Stage 2** — GPT-5.6 Sol·high +adversarial review, 20 findings (7 blockers), all verified against upstream jsr sources; 20/20 +accepted and integrated (`adversarial-sol.md`, `adversarial-triage.md`). **Stage 3** — Kimi K3 +docs-forecast (public guide + API references written as-if-shipped), 17 K-notes; 17/17 accepted +and integrated (`design/docs-story/`, `docs-story-triage.md`). The Wave-0 proof gates exist +because review demanded mechanism proof before contract freeze — the RFC inherits that +discipline rather than claiming false certainty. + +## 9. Open forks for owner arbitration + +| # | Fork | Options | Seed recommendation | +| --- | --- | --- | --- | +| F1 | Core pointer axis | (a) `.withFrontend()` pointer in `@netscript/plugin` (b) manifest-only, core untouched | **(a)** — doctrine-anticipated axis, zero vocabulary | +| F2 | Route mount default | (a) plugin-preferred base + host remap (b) forced `/plugins/<mountId>` namespace | **(a)** — `/auth/account` beats `/plugins/auth/account`; collisions caught deterministically | +| F3 | Contract package name | (a) `@netscript/plugin-frontend-core` (b) fold into `@netscript/fresh` (c) `@netscript/frontend-core` | **(a)** — symmetric with `plugin-*-core`, fresh-free contracts | +| F5 | Theme scope v1 | (a) CSS overlay only (b) + DTCG token merge | **(a)** | +| F7 | Wave-1 dogfood set | (a) workers only (b) workers + ai (c) all four | **(a) then (b)** | +| F8 | Milestone placement | beta.12 … beta.14 vs stable | defer — sequenced against PM #510 (beta.12), dashboard #400 (beta.13), deploy epics | +| F9 | SSR zone isolation | accept the v1 containment contract / fund an isolated-render protocol | **accept v1**; protocol only on demonstrated need | + +(F4 zones and F6 generator timing were removed from the fork list — review resolved them as +technical facts, not preferences.) + +--- + +<sub>**Provenance.** Seed run `plan-frontend-contrib--seed` on `plan/frontend-contrib`; this +document condenses the run's normative record (`design/canonical/00–06` rev 3, `plan.md`, +`design/examples/`, `design/docs-story/`). Where this RFC and the run docs conflict, the run +docs win until ratification, then GitHub wins. Refs #427 #432 #400 — no closing keywords; the +epic and sub-issues above are placeholders filed only after owner ratification.</sub> From e2eb34017338fa33e372ff83c6aa9f0f3fa6bb51 Mon Sep 17 00:00:00 2001 From: Rickylabs <eric.chautems@gmail.com> Date: Sun, 19 Jul 2026 07:55:30 +0200 Subject: [PATCH 09/11] plan(frontend-contrib): record RFC draft PR #890 in worklog Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015fc7SaneZJ9mzrAhRW4tma --- .llm/runs/plan-frontend-contrib--seed/worklog.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.llm/runs/plan-frontend-contrib--seed/worklog.md b/.llm/runs/plan-frontend-contrib--seed/worklog.md index a1da220be..7c8c1d4e1 100644 --- a/.llm/runs/plan-frontend-contrib--seed/worklog.md +++ b/.llm/runs/plan-frontend-contrib--seed/worklog.md @@ -98,3 +98,14 @@ STAGE-COMPLETE: adversarial-integration STAGE-COMPLETE: docs-story-integration — full kickoff pipeline done; awaiting owner review of plan.md forks (F1/F2/F3/F5/F7/F8/F9) and downstream filing decisions. + +## 2026-07-19 — Terminal deliverable: RFC draft PR + +- Owner directive (remote-control): open the lane's RFC draft PR modeled on #822; no issue + filing, no merge. +- `rfc.md` committed (`5691c018`); **draft PR #890** opened against `main` — + https://github.com/rickylabs/netscript/pull/890 — RFC body (topic, decision+rationale, public + API, plan/waves, placeholder board FCL-EPIC/FCB-1..24, supersession map, forks F1–F9). + Labels: `rfc` `type:docs` `status:plan` `area:fresh` `area:plugins` `priority:p1` + `ci:skip-e2e` `ci:skip-scaffold`; milestone **Backlog / Triage**; Refs #427/#432/#400 (no + closing keywords). Epic/sub-issue filing remains the later supervisor-coordinated step. From 0d40177abf4889a381fe02c331840c8b15cd09e0 Mon Sep 17 00:00:00 2001 From: Rickylabs <eric.chautems@gmail.com> Date: Sun, 19 Jul 2026 16:26:21 +0200 Subject: [PATCH 10/11] =?UTF-8?q?plan(frontend-contrib):=20board=20filed?= =?UTF-8?q?=20=E2=80=94=20epic=20#922=20+=20children=20#923-#946=20(FILING?= =?UTF-8?q?-LOG,=20labels.yml=20parity,=20authority=20banner)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015fc7SaneZJ9mzrAhRW4tma --- .github/labels.yml | 3 ++ .../plan-frontend-contrib--seed/FILING-LOG.md | 50 +++++++++++++++++++ .llm/runs/plan-frontend-contrib--seed/rfc.md | 8 ++- .../plan-frontend-contrib--seed/worklog.md | 11 ++++ 4 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 .llm/runs/plan-frontend-contrib--seed/FILING-LOG.md diff --git a/.github/labels.yml b/.github/labels.yml index 26c1165da..f9215ff12 100644 --- a/.github/labels.yml +++ b/.github/labels.yml @@ -199,6 +199,9 @@ - name: "epic:agentic-combo" color: "5319e7" description: "NetScript agentic combo epic (MCP server + public skills + CLI)" +- name: "epic:frontend-contrib" + color: "1d76db" + description: "Frontend contribution layer epic (plugins that ship UI)" # ── wave: release-train bucket (maps to milestone) ─────────────────────────── - name: "wave:v1" diff --git a/.llm/runs/plan-frontend-contrib--seed/FILING-LOG.md b/.llm/runs/plan-frontend-contrib--seed/FILING-LOG.md new file mode 100644 index 000000000..9ee047871 --- /dev/null +++ b/.llm/runs/plan-frontend-contrib--seed/FILING-LOG.md @@ -0,0 +1,50 @@ +# FILING-LOG — plan-frontend-contrib--seed (board filed 2026-07-19) + +Owner authorized epic + sub-issue creation in-turn (remote-control, 2026-07-19), including +milestone selection ("look on what milestone it fits best", deploy-lane split as precedent) and +new-milestone authority (not needed — existing milestones fit). + +**After this filing, GitHub is the single source of truth for the board.** Run docs carrying +FCB-n / FCL-EPIC tags are the planning record; on conflict, GitHub wins. + +- Label created: `epic:frontend-contrib` (#1d76db) — mirrored into `.github/labels.yml` (this branch). +- Epic: **#922** — `Epic: Frontend contribution layer — plugins that ship UI` · milestone `0.0.1-beta.13`. +- Milestone rationale: core waves (0/1/1b/2 + first-party panels) → **beta.13** — the layer is the + dashboard epic's (#400) prerequisite in the same cut, mirroring the deploy lane's W1–W3→beta.13 + split (epic #892); consumer frontends + adoption → **beta.15** (parallel to deploy W4); + backend prerequisite + sugar → **Backlog / Triage**. No new milestone required. +- Supersession map (RFC §6): #427 KEEP re-baseline · #432 KEEP re-baseline · #400 consumer · + ai chat-route repositioned — **zero closes at filing time** (folds happen via downstream PRs). + +## Draft-ID → live issue + +| Draft | Issue | Milestone | Priority | Title | +| --- | --- | --- | --- | --- | +| FCB-1 | #923 | 0.0.1-beta.13 | p0 | P1 proof: mounted sub-app command ordering | +| FCB-2 | #924 | 0.0.1-beta.13 | p0 | P2 proof: literal lazy route loaders + normalizeFreshRouteModule | +| FCB-3 | #925 | 0.0.1-beta.13 | p0 | P3 proof: dependency-island build matrix + plugin-vite pin policy | +| FCB-4 | #926 | 0.0.1-beta.13 | p0 | P4 proof: SSR failure-containment fixtures | +| FCB-5 | #927 | 0.0.1-beta.13 | p0 | P5 proof: gateway threat model + streaming abort | +| FCB-6 | #928 | 0.0.1-beta.13 | p0 | @netscript/plugin-frontend-core contracts/v1 | +| FCB-7 | #929 | 0.0.1-beta.13 | p0 | @netscript/plugin pointer axis (.withFrontend) | +| FCB-8 | #930 | 0.0.1-beta.13 | p0 | Frontend registry emissions: transactional replace-set | +| FCB-9 | #931 | 0.0.1-beta.13 | p0 | @netscript/fresh/plugins host runtime | +| FCB-10 | #932 | 0.0.1-beta.13 | p1 | Scaffold template wiring + HostSurfaceDescriptor + vite feed | +| FCB-11 | #933 | 0.0.1-beta.13 | p1 | Workers dogfood: zone panel + console route + island | +| FCB-12 | #934 | 0.0.1-beta.13 | p1 | Generated deny-by-default procedure gateway | +| FCB-13 | #935 | 0.0.1-beta.13 | p2 | plugin new --with frontend | +| FCB-14 | #936 | 0.0.1-beta.13 | p1 | netscript plugin dev (frontend watch loop) | +| FCB-15 | #937 | 0.0.1-beta.13 | p2 | Doctor frontend check + five-state taxonomy | +| FCB-16 | #938 | 0.0.1-beta.13 | p2 | Quarantine render states + provenance chrome | +| FCB-17 | #939 | 0.0.1-beta.15 | p1 | AppTarget scaffolder seam + plugin resource add --app | +| FCB-18 | #940 | 0.0.1-beta.13 | p1 | defineFrontendTestSuite + budgets enforcement | +| FCB-19 | #941 | 0.0.1-beta.15 | p2 | generate frontend-wiring adoption verb | +| FCB-20 | #942 | 0.0.1-beta.15 | p1 | Auth v1 frontend (account + session widget + signin starter) | +| FCB-21 | #943 | 0.0.1-beta.15 | p1 | AI frontend (durable chat route + assist launcher) | +| FCB-22 | #944 | 0.0.1-beta.13 | p2 | Sagas/triggers/streams dashboard-zone panels | +| FCB-23 | #945 | Backlog / Triage | p2 | auth-org backend capability (org-console prerequisite) | +| FCB-24 | #946 | Backlog / Triage | p3 | Convention generator (generate frontend) | + +Filing was one-shot from the committed manifest (`briefs`/tmp manifest mirrored here); every +child carries `Part of #922`, `epic:frontend-contrib`, one `status:plan`, `type:`/`area:`/ +`priority:`, and its milestone. First pull: p0s #923–#931. diff --git a/.llm/runs/plan-frontend-contrib--seed/rfc.md b/.llm/runs/plan-frontend-contrib--seed/rfc.md index 7233d4fc9..45d5da0e7 100644 --- a/.llm/runs/plan-frontend-contrib--seed/rfc.md +++ b/.llm/runs/plan-frontend-contrib--seed/rfc.md @@ -1,9 +1,13 @@ # RFC — NetScript Frontend Contribution Layer: plugins that ship UI +> **AUTHORITY BANNER (2026-07-19): the board is FILED — GitHub wins on conflict.** Epic **#922**, +> children **#923–#946** (FCB-1..24 mapping in `FILING-LOG.md`). The FCB-n / FCL-EPIC tags below +> are the planning record, not the live board. + | | | | --- | --- | -| **Status** | **Proposed** — full pipeline complete (generator → GPT-5.6 Sol·high adversarial, 20/20 integrated → Kimi K3 docs story, 17/17 integrated → rev 3); awaiting owner ratification of the open forks (§9) | -| **Tracking** | Refs #427 (dashboard panel seam) · #432 (add-to-app scaffold flow) · #400 (dev-dashboard epic, beta.13) · **Epic FCL-EPIC (placeholder — filed in a later supervisor-coordinated step)** · sibling RFC: deploy-plugin lane (`plan/deploy-plugin`) | +| **Status** | **Proposed** — full pipeline complete (generator → GPT-5.6 Sol·high adversarial, 20/20 integrated → Kimi K3 docs story, 17/17 integrated → rev 3); board filed under owner authorization (2026-07-19); forks (§9) still open for arbitration | +| **Tracking** | Refs #427 (dashboard panel seam) · #432 (add-to-app scaffold flow) · #400 (dev-dashboard epic, beta.13) · **Epic #922, children #923–#946** (`FILING-LOG.md`) · sibling RFC: deploy-plugin lane (#891, epic #892) | | **Run record** | `.llm/runs/plan-frontend-contrib--seed/` — research, plan (D1–D16 / forks), canonical design 00–06, 4 worked examples, docs-story forecast, both review-triage records | | **Evidence base** | Upstream verified in-source: `App.mountApp` (@fresh/core 2.3.3 app.ts:357), `Builder.registerIsland` (dev/builder.ts:157), `fresh({ islandSpecifiers })` (@fresh/plugin-vite 1.0.8 mod.ts:211-214); repo precedent: axis registry emitter, `scaffolder.export` pointer, fresh-ui copy-registry + `--ns-*` tokens, ai chat-route scaffolder; prior art: `dashboard-design--orchestrator/analysis/plugin-extension-architecture.md` | diff --git a/.llm/runs/plan-frontend-contrib--seed/worklog.md b/.llm/runs/plan-frontend-contrib--seed/worklog.md index 7c8c1d4e1..187e05783 100644 --- a/.llm/runs/plan-frontend-contrib--seed/worklog.md +++ b/.llm/runs/plan-frontend-contrib--seed/worklog.md @@ -109,3 +109,14 @@ plan.md forks (F1/F2/F3/F5/F7/F8/F9) and downstream filing decisions. Labels: `rfc` `type:docs` `status:plan` `area:fresh` `area:plugins` `priority:p1` `ci:skip-e2e` `ci:skip-scaffold`; milestone **Backlog / Triage**; Refs #427/#432/#400 (no closing keywords). Epic/sub-issue filing remains the later supervisor-coordinated step. + +## 2026-07-19 — Board FILED (owner-authorized) + +- Owner authorized epic/sub-issue creation + milestone selection in-turn. Label + `epic:frontend-contrib` created (+ labels.yml parity, this branch). **Epic #922** (beta.13); + children **#923–#946** filed one-shot from the manifest: 18 → beta.13 (Waves 0/1/1b/2 + + first-party panels — the dashboard #400 prerequisite, mirroring deploy W1–W3→beta.13), 4 → + beta.15 (AppTarget, adoption verb, auth v1, ai), 2 → Backlog (auth-org prerequisite, + convention generator). No new milestone needed. Zero closes at filing (supersession map: + KEEP-re-baseline). `FILING-LOG.md` committed; rfc.md carries the authority banner; PR #890 + body patched + phase comment posted. First pull: p0s #923–#931. GitHub is now board authority. From cae3335c6621824123ed5df303e61bbe3f71ede2 Mon Sep 17 00:00:00 2001 From: Rickylabs <eric.chautems@gmail.com> Date: Sun, 19 Jul 2026 16:29:47 +0200 Subject: [PATCH 11/11] =?UTF-8?q?plan(frontend-contrib):=20board=20amendme?= =?UTF-8?q?nt=20=E2=80=94=20beta.17=20completion=20wave=20replaces=20Backl?= =?UTF-8?q?og=20placements?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015fc7SaneZJ9mzrAhRW4tma --- .llm/runs/plan-frontend-contrib--seed/FILING-LOG.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.llm/runs/plan-frontend-contrib--seed/FILING-LOG.md b/.llm/runs/plan-frontend-contrib--seed/FILING-LOG.md index 9ee047871..912196311 100644 --- a/.llm/runs/plan-frontend-contrib--seed/FILING-LOG.md +++ b/.llm/runs/plan-frontend-contrib--seed/FILING-LOG.md @@ -12,7 +12,8 @@ FCB-n / FCL-EPIC tags are the planning record; on conflict, GitHub wins. - Milestone rationale: core waves (0/1/1b/2 + first-party panels) → **beta.13** — the layer is the dashboard epic's (#400) prerequisite in the same cut, mirroring the deploy lane's W1–W3→beta.13 split (epic #892); consumer frontends + adoption → **beta.15** (parallel to deploy W4); - backend prerequisite + sugar → **Backlog / Triage**. No new milestone required. + completion wave → **beta.17** (milestone #19, created 2026-07-19 per owner re-scheduling: no + Backlog items — everything ships before stable). - Supersession map (RFC §6): #427 KEEP re-baseline · #432 KEEP re-baseline · #400 consumer · ai chat-route repositioned — **zero closes at filing time** (folds happen via downstream PRs). @@ -42,9 +43,11 @@ FCB-n / FCL-EPIC tags are the planning record; on conflict, GitHub wins. | FCB-20 | #942 | 0.0.1-beta.15 | p1 | Auth v1 frontend (account + session widget + signin starter) | | FCB-21 | #943 | 0.0.1-beta.15 | p1 | AI frontend (durable chat route + assist launcher) | | FCB-22 | #944 | 0.0.1-beta.13 | p2 | Sagas/triggers/streams dashboard-zone panels | -| FCB-23 | #945 | Backlog / Triage | p2 | auth-org backend capability (org-console prerequisite) | -| FCB-24 | #946 | Backlog / Triage | p3 | Convention generator (generate frontend) | +| FCB-23 | #945 | 0.0.1-beta.17 | p2 | auth-org backend capability (org-console prerequisite) | +| FCB-24 | #946 | 0.0.1-beta.17 | p3 | Convention generator (generate frontend) | Filing was one-shot from the committed manifest (`briefs`/tmp manifest mirrored here); every child carries `Part of #922`, `epic:frontend-contrib`, one `status:plan`, `type:`/`area:`/ `priority:`, and its milestone. First pull: p0s #923–#931. + +**Amendment 2026-07-19 (owner):** milestone `0.0.1-beta.17` (#19) created; #945/#946 moved Backlog → beta.17. Final split: 18 → beta.13 · 4 → beta.15 · 2 → beta.17.