Skip to content

Commit 22e234f

Browse files
docs: define context plugin boundary
1 parent 5b8d0b4 commit 22e234f

1 file changed

Lines changed: 249 additions & 0 deletions

File tree

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
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

Comments
 (0)