|
| 1 | +# Context Plugin Boundary Design |
| 2 | + |
| 3 | +<!-- cspell:ignore Kiali Midscene --> |
| 4 | + |
| 5 | +**Status:** Approved |
| 6 | + |
| 7 | +**Related work:** |
| 8 | + |
| 9 | +- Rstack CLI plugin SPI: [rstackjs/rstack-cli#336](https://github.com/rstackjs/rstack-cli/pull/336) |
| 10 | +- Rstack Context integration: [rstackjs/rstack-cli#344](https://github.com/rstackjs/rstack-cli/pull/344) |
| 11 | +- Standalone Context package: [rstackjs/context#1](https://github.com/rstackjs/context/pull/1) |
| 12 | +- Agent plugin bundle: [rstackjs/agent-skills#102](https://github.com/rstackjs/agent-skills/pull/102) |
| 13 | + |
| 14 | +## Goal |
| 15 | + |
| 16 | +Make `@rstackjs/context` an independently usable context runtime with no dependency on the `rstack` |
| 17 | +package or its internal modules. Rstack CLI depends on Context and consumes its public `./rstack` |
| 18 | +entry point through the plugin SPI from PR #336. |
| 19 | + |
| 20 | +The integration should be concentrated in a small CLI-owned host adapter. Direct consumers must still |
| 21 | +be able to use the individual Rsbuild, Rslib, Rstest, Rslint, Rsdoctor, storage, and MCP capabilities |
| 22 | +without adopting Rstack CLI. |
| 23 | + |
| 24 | +## Dependency direction |
| 25 | + |
| 26 | +```mermaid |
| 27 | +flowchart LR |
| 28 | + Agent["Codex / Claude plugin bundle"] -->|"launches rs mcp"| CLI["rstack CLI"] |
| 29 | + CLI -->|"imports public entry points"| Context["@rstackjs/context"] |
| 30 | + CLI -->|"implements public SPI"| SPI["Rstack plugin SPI"] |
| 31 | + Context --> Rsbuild["Rsbuild / Rslib hooks"] |
| 32 | + Context --> Rstest["Rstest capture / reporter surface"] |
| 33 | + Context --> Rslint["Rslint capture surface"] |
| 34 | + Context -->|"consumes artifacts through agent-cli"| Rsdoctor["Rsdoctor"] |
| 35 | + Context --> MCP["Context MCP server"] |
| 36 | +
|
| 37 | + Context -. "must not import" .-> CLI |
| 38 | + Agent -. "must not bundle a second runtime" .-> Context |
| 39 | +``` |
| 40 | + |
| 41 | +The dashed Context-to-CLI edge is forbidden. `@rstackjs/context` must not declare a runtime, peer, |
| 42 | +development, or type-only package dependency on `rstack`. |
| 43 | + |
| 44 | +## Package entry points |
| 45 | + |
| 46 | +Context keeps its root barrel for compatibility and adds focused public entry points: |
| 47 | + |
| 48 | +| Entry point | Responsibility | |
| 49 | +| ---------------------------- | ----------------------------------------------------------------------------------------------------------------- | |
| 50 | +| `@rstackjs/context` | Backward-compatible aggregate API and shared evidence/store types | |
| 51 | +| `@rstackjs/context/rsbuild` | Rsbuild build observer plugin, config append helper, and build types | |
| 52 | +| `@rstackjs/context/rslib` | Rslib-facing contributor built on the same Rsbuild environment hooks | |
| 53 | +| `@rstackjs/context/rstest` | Explicit Rstest capture API, related-test contracts, execution evidence, and reporter surface when used | |
| 54 | +| `@rstackjs/context/rslint` | Explicit Rslint capture API and diagnostic/fix-preview queries | |
| 55 | +| `@rstackjs/context/rsdoctor` | Consumer APIs for Rsdoctor artifact discovery, graph normalization, analysis, and reports; not an Rsdoctor plugin | |
| 56 | +| `@rstackjs/context/mcp` | Context MCP server and host-dependency contracts | |
| 57 | +| `@rstackjs/context/rstack` | Combined Rstack plugin entry point composed from the focused APIs | |
| 58 | + |
| 59 | +The focused entry points expose real Context capabilities; they are not wrappers around `rs` commands. |
| 60 | +They remain usable in repositories that adopt only one producer. Missing producers degrade to |
| 61 | +unavailable evidence and do not prevent other entry points from loading. |
| 62 | + |
| 63 | +The Rstest subpath initially exports the supported one-shot capture and execution-evidence surfaces. |
| 64 | +Any watch reporter added later belongs on this subpath and must keep static relation, test outcome, |
| 65 | +and runtime execution evidence separate. |
| 66 | + |
| 67 | +## Rsdoctor boundary |
| 68 | + |
| 69 | +The dependency direction is one way: Context consumes Rsdoctor; Rsdoctor does not consume Context. |
| 70 | +Context does not export an Rsdoctor build plugin and Rsdoctor does not import |
| 71 | +`@rstackjs/context`. |
| 72 | + |
| 73 | +Rsdoctor's own build plugin produces `rsdoctor-data.json`. Context reads that completed artifact |
| 74 | +through the pinned `@rsdoctor/agent-cli` adapter, normalizes the relevant graph and section evidence, |
| 75 | +and exposes the results through `@rstackjs/context/rsdoctor` and the Context MCP server. The |
| 76 | +`./rsdoctor` name describes this consumer-facing Context API, not a plugin installed into Rsdoctor. |
| 77 | + |
| 78 | +## Rstack entry point without reverse coupling |
| 79 | + |
| 80 | +`@rstackjs/context/rstack` exports `createRstackContextPlugin(options)`. The returned object is |
| 81 | +structurally compatible with the public Rstack plugin SPI (`name` plus `setup`) but Context defines |
| 82 | +the minimal structural contract locally. It does not import types or runtime code from `rstack`. |
| 83 | + |
| 84 | +Rstack CLI performs a compile-time compatibility assignment at its adapter boundary: |
| 85 | + |
| 86 | +```ts |
| 87 | +import { createRstackContextPlugin } from '@rstackjs/context/rstack'; |
| 88 | +import type { RstackPlugin } from './plugin.ts'; |
| 89 | + |
| 90 | +const plugin: RstackPlugin = createRstackContextPlugin(options); |
| 91 | +``` |
| 92 | + |
| 93 | +If the SPI changes incompatibly, the Rstack CLI build fails at this one boundary. Context itself can |
| 94 | +still build, test, and publish without `rstack` installed. |
| 95 | + |
| 96 | +## Stacking on the plugin SPI |
| 97 | + |
| 98 | +PR #344 is stacked on PR #336. Context is registered as an internal Rstack plugin, not a plugin that |
| 99 | +users must repeat in `define.plugins()`. |
| 100 | + |
| 101 | +The plugin runtime composes plugins in this order: |
| 102 | + |
| 103 | +1. user plugins in declared order; |
| 104 | +2. the internal Context plugin. |
| 105 | + |
| 106 | +This preserves the user-plugin ordering contract and lets Context observe the final native build |
| 107 | +configuration without becoming a special case in every tool loader. `define.context()` remains the |
| 108 | +small first-party configuration surface for enabling capture, selecting metadata/deep capture, and |
| 109 | +setting a variant. |
| 110 | + |
| 111 | +The SPI gains one small typed addition: configuration modifier handlers receive native invocation |
| 112 | +context as their second argument. The map is specific to each tool kind, so application, library, and |
| 113 | +test modifiers receive their native `ConfigParams`; other kinds receive an empty context until they |
| 114 | +have a concrete need. Existing one-argument modifiers remain source-compatible. |
| 115 | + |
| 116 | +This is needed because build evidence must use the native command and environment mode supplied by |
| 117 | +Rsbuild or Rslib. It must not infer them from `process.argv`. |
| 118 | + |
| 119 | +## CLI-owned adapter |
| 120 | + |
| 121 | +Rstack-specific behavior is consolidated in one internal module, tentatively |
| 122 | +`packages/rstack/src/contextPlugin.ts`. It owns only the host knowledge Context cannot own: |
| 123 | + |
| 124 | +- loading `define.context()` from the selected Rstack config; |
| 125 | +- supplying the loaded config path and dependency files; |
| 126 | +- assigning the Context plugin to `RstackPlugin` at compile time; |
| 127 | +- supplying Rstack's Rslint and Rstest wrapper-config paths; |
| 128 | +- running explicit capture inside `withRstackConfigTarget()`; |
| 129 | +- detecting whether `define.test()` exists for a selected package; |
| 130 | +- resolving related tests through the current `rs test list` behavior; |
| 131 | +- connecting the MCP server to stdio while reserving stdout for JSON-RPC. |
| 132 | + |
| 133 | +The rest of the CLI sees narrow functions from this module rather than importing Context primitives |
| 134 | +throughout tool loaders. |
| 135 | + |
| 136 | +Expected long-term CLI coupling is limited to: |
| 137 | + |
| 138 | +1. the package dependency and public `rstack/context` re-export; |
| 139 | +2. `ContextConfig` in `Configs` and `define.context()`; |
| 140 | +3. internal plugin registration in the plugin runtime; |
| 141 | +4. built-in `rs mcp` dispatch to the CLI-owned adapter. |
| 142 | + |
| 143 | +`rs mcp` remains a built-in command rather than a project-configured plugin command. This reserves |
| 144 | +stdout before the MCP transport starts and allows the server to launch even when no Rstack config is |
| 145 | +present. Moving it behind ordinary plugin-command discovery would unnecessarily execute project |
| 146 | +configuration before protocol setup. |
| 147 | + |
| 148 | +## Build integration |
| 149 | + |
| 150 | +The Context Rstack plugin registers `app` and `lib` config modifiers. Each modifier: |
| 151 | + |
| 152 | +1. resolves the Context workspace from the loaded config or invocation root; |
| 153 | +2. records the loaded Rstack config and dependency inputs once; |
| 154 | +3. creates the appropriate Context build observer with native `ConfigParams`; |
| 155 | +4. appends exactly one observer to the resolved native plugin list; |
| 156 | +5. preserves the user's config object and existing plugins. |
| 157 | + |
| 158 | +The application modifier records `producer: 'rsbuild'` and `product: 'application'`. The library |
| 159 | +modifier records `producer: 'rslib'` and `product: 'library'`. Repositories may use either or both. |
| 160 | + |
| 161 | +Rstest automatic `extends` continues to apply application or library modifiers through the normal |
| 162 | +SPI path. Explicit `extends` keeps its existing opt-out semantics. Tests must prove that the observer |
| 163 | +is not duplicated when automatic inheritance and a native build command both occur in separate |
| 164 | +invocations. |
| 165 | + |
| 166 | +## Explicit lint and test capture |
| 167 | + |
| 168 | +Lint and test capture remain Context-owned operations exposed through the MCP dependency contract. |
| 169 | +The CLI adapter provides only host callbacks: |
| 170 | + |
| 171 | +```text |
| 172 | +Context capture algorithm |
| 173 | + -> asks host to select/load Rstack config |
| 174 | + -> invokes Rslint or Rstest through the supplied wrapper config |
| 175 | + -> stores normalized evidence in Context |
| 176 | +``` |
| 177 | + |
| 178 | +This keeps Context independent from Rstack config internals while preserving correct per-package |
| 179 | +selection in monorepos. A project with no Rstest configuration can still use build, Rsdoctor, and |
| 180 | +Rslint evidence. A project with no Rslint configuration can still use every other available axis. |
| 181 | + |
| 182 | +## Agent plugin bundle |
| 183 | + |
| 184 | +The Agent Skills repository does not depend on or bundle another Context runtime. Its local stdio |
| 185 | +launcher discovers the workspace's `rstack` executable and runs `rs mcp`. Skills describe workflows |
| 186 | +over the MCP contract. |
| 187 | + |
| 188 | +This keeps one runtime and one store per checkout: |
| 189 | + |
| 190 | +```text |
| 191 | +Agent skill -> installed plugin launcher -> workspace rs mcp |
| 192 | + -> Rstack host adapter -> @rstackjs/context |
| 193 | +``` |
| 194 | + |
| 195 | +Plugin tests should use Rstack and Context preview packages, but published plugin metadata remains |
| 196 | +repository-based rather than an npm dependency graph. |
| 197 | + |
| 198 | +## Preview dependency policy |
| 199 | + |
| 200 | +Published Context previews must contain ordinary semver dependencies. A pkg.pr.new URL must not be |
| 201 | +published as a transitive dependency because repositories using `blockExoticSubdeps` correctly |
| 202 | +reject it. |
| 203 | + |
| 204 | +Context may validate against a Rsdoctor preview through a root-only pnpm override. The packed Context manifest |
| 205 | +must retain the supported semver dependency. Rstack CLI then depends on the latest Context preview |
| 206 | +for cross-repository validation. |
| 207 | + |
| 208 | +## Compatibility and tests |
| 209 | + |
| 210 | +The refactor preserves the current root exports and MCP schemas. Required validation includes: |
| 211 | + |
| 212 | +- Context builds and tests with no `rstack` package installed; |
| 213 | +- every focused Context subpath imports independently; |
| 214 | +- the packed Context manifest contains no exotic transitive dependency; |
| 215 | +- Rstack compile-time compatibility for `createRstackContextPlugin()`; |
| 216 | +- one Context observer for Rsbuild-only, Rslib-only, and mixed configurations; |
| 217 | +- correct native command/mode/config provenance from modifier invocation context; |
| 218 | +- no Context observer when capture is off; |
| 219 | +- explicit lint/test MCP capture still uses the selected package/config; |
| 220 | +- missing Rstest or Rslint degrades without affecting build/Rsdoctor queries; |
| 221 | +- `rs mcp` works without project configuration; |
| 222 | +- Agent plugin launcher works against the stacked Rstack/Context previews. |
| 223 | + |
| 224 | +After repository tests pass, validate the installed plugin against at least: |
| 225 | + |
| 226 | +- HeaderEditor for Rsbuild-only graceful degradation; |
| 227 | +- Kiali for Rsbuild plus Rstest; |
| 228 | +- Midscene for Rsbuild, Rslib, and Rstest in a monorepo; |
| 229 | +- one Rspack-only or Rspack 1.x project to verify unsupported axes remain explicit. |
| 230 | + |
| 231 | +## Alternatives considered |
| 232 | + |
| 233 | +### Keep bespoke calls in every tool loader |
| 234 | + |
| 235 | +This requires fewer immediate changes but leaves Context behavior spread across Rsbuild, Rslib, MCP, |
| 236 | +and config modules. It duplicates the composition mechanism added by PR #336 and makes future |
| 237 | +producer entry points harder to add consistently. |
| 238 | + |
| 239 | +### Make Context depend on `rstack` |
| 240 | + |
| 241 | +Context could import `RstackPlugin` directly and declare a peer dependency. This creates a reverse |
| 242 | +package edge and prevents Context from being a genuinely standalone producer/MCP runtime. Structural |
| 243 | +compatibility at the CLI boundary provides the same type check without the cycle. |
| 244 | + |
| 245 | +### Run Context only through subprocess commands |
| 246 | + |
| 247 | +This minimizes in-process imports but loses native configuration parameters and build lifecycle |
| 248 | +hooks, weakens monorepo selection, and adds transport/process complexity. The plugin SPI plus native |
| 249 | +tool hooks is both simpler and more accurate. |
0 commit comments