@rackbops/ac-plugin-contract 2.0.0-alpha.42 → 2.0.0-alpha.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AUTHORING.md CHANGED
@@ -113,7 +113,12 @@ Rules the host enforces, at validation or load time:
113
113
  installed plugin errors, naming the bundled owner — and the bundled one keeps running unaffected. This
114
114
  is what stops a third-party package from disabling a bundled plugin (e.g. the auth provider, or `admin`
115
115
  itself) just by declaring its id. Two bundled plugins sharing an id is still a build/CI mistake and both
116
- error, the same for two installed plugins with no bundled plugin in the collision.
116
+ error, the same for two installed plugins with no bundled plugin in the collision. **An installed plugin
117
+ may neither provide nor consume the `auth` slot** (#452b, #604): the install gate (`ac plugins install`
118
+ and the admin Plugins card's install, which share it) refuses a manifest whose `contributes.provides` or
119
+ `contributes.consumes` names `auth` (any version), and
120
+ at runtime `slots.provide`/`slots.resolve` refuse that slot to an installed plugin whatever its manifest
121
+ declares. Every other slot, `notify@1` included, is open to an installed plugin.
117
122
  - **`hostApiVersion`** must equal the host's `HOST_API_VERSION` (currently `1`); a mismatch is rejected.
118
123
  - **`kind: "in-process"`** must declare a `server` entry — the *loader* records an error and skips a plugin that
119
124
  doesn't (the schema itself requires only `id`, `hostApiVersion` and `kind`). `contributes` and every sub-field
@@ -187,10 +192,16 @@ export const createPlugin: CreatePlugin = (host) => ({
187
192
  click, whether or not the manifest sets `confirm` -- there is no second "Confirm" click. `danger: true` gives
188
193
  that primary button the danger style, gives its Ctrl/Cmd+K palette row a danger badge, and stops Enter pressed in
189
194
  one of the form's fields from submitting it (a click on the button, or Enter or Space on the focused button,
190
- still fires; Enter in a text area is a newline). A non-danger action still submits on Enter in any field,
191
- including a checkbox or a radio (browsers treat that Enter as a submit): mark an action `danger` if it must
192
- not fire from a stray Enter. The primary disables only while a request is in flight, so an action that
193
- answers faster than an operator's second click can be run twice: make its `run` tolerate an overlapping call.
195
+ still fires; Enter in a text area is a newline). A non-danger action still submits on Enter in a text field;
196
+ Enter on a checkbox, a radio or a select never submits any action (#568), so ticking a label and pressing
197
+ Enter does not fire it: mark an action `danger` if it must not fire from a stray Enter in a text field either.
198
+ After a successful result the primary stays disabled until the modal closes, so an action that succeeded is
199
+ never run again from that modal, however fast it answers (a double-click whose first run resolves inside the
200
+ second click's interval, or a repeated Enter, cannot run it twice). The modal closes itself 1.5 s after the
201
+ success unless the user clicks in it first, and then it stays open, locked, until Cancel or Escape; opening the
202
+ same action again while that locked modal is up reuses it and does not re-run it. After a failed result the
203
+ primary is enabled again and a retry runs, so a double-click whose first run fails fast can run the action
204
+ twice (the second a retry): an action whose failure may have partly applied should tolerate that.
194
205
  `confirm` stays a valid manifest flag, so an existing manifest keeps loading, but neither the modal nor the
195
206
  palette acts on it.
196
207
  - **Job** — implement `runJob(ctx)` and dispatch on `ctx.jobId`. The host runs only jobs you declared in
@@ -434,6 +445,31 @@ strip of the panel uncovered between the header and the overlay; and the panel i
434
445
  the bar collapses. The layout arithmetic and the whole-pixel rounding were measured in desktop Chromium at
435
446
  375 px; the phone behaviours in this list are derived from the CSS, not observed on a phone.
436
447
 
448
+ ### A frontend that bundles a dependency (#98)
449
+
450
+ "Only the import-map externals" is what keeps one React on the page; it does not forbid a plugin from
451
+ bundling a library the import map does not carry. `plugins/ac-artifacts-weft` inlines
452
+ `@lepid-labs/weft-embed` (a framework-free reader, 1.15 MB) and its stylesheet into `dist/ui.js`, and its
453
+ `rollup.ui.config.mjs` is the reference. (The rollup configs of `issues-classic` and `ac-auth-cf-access`
454
+ inline a dependency into their SERVER bundle, and every other plugin's UI config inlines nothing; none of
455
+ them is a model for this.) What a bundled dependency changes:
456
+
457
+ - **One file.** The host serves exactly the manifest's `frontend` and never a sibling, so a stylesheet is a
458
+ string in the bundle that the panel injects as a `<style>`, once (the config's `inlineCss` plugin turns
459
+ `import css from "pkg/style.css"` into that string), never a `<link>` to a `.css` file.
460
+ - **Externals stay bare, everything else is inlined** (`@rollup/plugin-node-resolve`). The dependency goes in
461
+ `dependencies`, pinned exactly if its rendering is part of what you ship, and is absent at runtime: the
462
+ image carries `dist/` and no `node_modules`.
463
+ - **A library without types** gets a `src/<lib>.d.ts` that declares the module, listed in the UI tsconfigs.
464
+ - **Guard what shipped with a bundle test that runs the real build** (`src/bundle.test.ts` is the example):
465
+ anchor the import regex to the start of a line (inlined code carries doc comments such as ` * import
466
+ {unified} from 'unified'`), assert that no bare specifier outside the externals survives and that the
467
+ library is not one, that nothing needs `require(`, `import.meta` or `eval`, what the bundle exports, and a
468
+ size budget.
469
+ - **A stylesheet you did not write must not restyle the shell.** Check its selectors for what is scoped (a
470
+ parser over the CSS is enough: weft's 0.2.0 rules are all scoped to its own root), and have the smoke spec
471
+ compare the shell's computed styles with and without it in the page.
472
+
437
473
  ### The `@ac/host` browser module
438
474
 
439
475
  `@ac/host` (typed as `BrowserHostApi` in `@ac/contract`) is the one shared module instance the shell and every
@@ -556,10 +592,13 @@ same-origin iframe (the Artifacts report preview does) forwards the hotkey: bind
556
592
  iframe's `contentDocument` (skip a frame whose `contentDocument` is null or throws: cross-origin), and on Ctrl
557
593
  or Cmd plus `k` with no Alt and no Shift, `preventDefault()` and dispatch `new CustomEvent("ac:quickcast-key")`
558
594
  on the parent `window`. The shell opens the palette under the keystroke's own guard: only when no other dialog
559
- is open, and an event while the palette is already open does nothing (closing stays with Ctrl/Cmd+K and
560
- Escape). Remove the listener when the frame goes away. Closing the palette with Escape leaves focus on the
561
- shell page rather than back inside the frame (observed in Chromium), so the user clicks the frame to type in
562
- it again.
595
+ is open, and an event while the palette is already open leaves it open (closing stays with Ctrl/Cmd+K and
596
+ Escape). Remove the listener when the frame goes away. Closing the palette with Escape or Ctrl/Cmd+K again
597
+ returns focus to the frame element (#588): the native dialog's own restore does not put it back there for a
598
+ palette opened from inside a frame (observed in Chromium), so the shell remembers the element that had focus
599
+ when the event arrived and focuses it again when focus is nowhere useful after the close (on `body`, or
600
+ still on a node of the palette that just closed). Which element inside the frame's own document has focus
601
+ afterwards is the browser's call.
563
602
 
564
603
  `fetch` makes a same-origin, credentialed, CSRF-aware request to a host/plugin route — a relative `path`
565
604
  resolves against the console origin; a cross-origin absolute URL throws rather than being followed. A panel
@@ -997,7 +1036,25 @@ integer `rank` and the shell only sorts by it. The rule, in priority order:
997
1036
  Ties break the same way, so the order is total. `order` is a hint the operator can always override with
998
1037
  `tab_order`, so leave it off unless your panel belongs at the front (the Board declares `10`, Issues `20`,
999
1038
  which puts them right after Home). `Home` is always first and is not orderable. A panel that lost a
1000
- contested nav slot still has no tab, and still appears in quickcast at its ranked position.
1039
+ contested nav slot still has no tab, and still appears in quickcast at its ranked position (once the
1040
+ operator's choice has flipped the slot, the position the other panel's tab would hold: the two swap ranks, #629).
1041
+
1042
+ **Contesting a nav slot (#65, #100).** A panel's `nav` names the slot it competes for the tab of. A slot with
1043
+ one panel is owned by it. When two or more panels from plugins that loaded without error name the same slot,
1044
+ the host picks one owner per request: the operator's choice, stored in the `nav.<slot>.panel` host setting
1045
+ (written through `POST /api/nav-slot` by the header's switcher, which renders only for a contested slot; an
1046
+ `AC_NAV_<SLOT>_PANEL` environment variable, when set, wins over it and pins the slot: the switcher for it is
1047
+ disabled, naming the variable, and `POST /api/nav-slot` answers `409`), else the smallest panel id. A panel
1048
+ that does not own its slot has no tab but keeps its route and its quickcast
1049
+ entry. The slot's tab stays where the default owner's would be, whichever panel owns it (#629), so a
1050
+ flip changes what the tab shows, not where it sits (a `tab_order` that names the chosen owner places it
1051
+ where the operator said). So declaring a `nav` another panel already has changes two things for every operator: the header gains
1052
+ the switcher, and the default owner is whichever panel id sorts first (an id that sorts after the existing
1053
+ owner leaves the default alone; one that sorts before it takes the tab from every operator who has not made a
1054
+ choice, and, since #629, also moves the slot's tab for an operator who has: its position is where the default
1055
+ owner's would be, and the default owner is the smallest id). The bundled example is the `artifacts` slot: the native viewer's `viewer` (`plugins/artifacts`, route
1056
+ `/artifacts`, the default) and the weft reader's `weft-reader` (`plugins/ac-artifacts-weft`, route
1057
+ `/artifacts-weft`).
1001
1058
 
1002
1059
  **`isolation` (#124).** A panel may declare `isolation: "iframe"`: the value is reserved (design §4) and the
1003
1060
  manifest accepts it, but nothing implements it in 2.0 and the host ignores it -- panels run in-process today,
@@ -1017,7 +1074,14 @@ refuses to activate a consumer whose slot has no provider, and says why (`resolv
1017
1074
  `packages/host/src/plugins/slots.ts`, re-exported from `plugins/index.ts`). `ac-auth-cf-access`
1018
1075
  (`auth@1`) and `ac-notify-discord` (`notify@1`) are the reference *providers*; see their own
1019
1076
  `src/server.ts` for `host.slots.provide(slot, version, impl)`, and `design.md` §4 for the slot
1020
- contract itself. `hello` contributes none of the six points above via a slot (nor `attention` -- its own manifest
1077
+ contract itself. The consumer half is `host.slots.resolve<T>(slot, version)` (#97): the provider
1078
+ bound for a slot, or `null` when none is. It throws unless the plugin's own manifest lists the exact
1079
+ `{slot, version}` in `contributes.consumes`, and an installed plugin may not resolve `auth` at all. Call
1080
+ it at request time (inside a route handler or job), never in `activate()`: providers bind during
1081
+ activation in load order, so a consumer that activates first would read `null` there for good.
1082
+ `ac-artifacts-weft` (`artifacts@1`) is the reference consumer: the second plugin to declare `consumes`
1083
+ for it, after `artifacts`, which reads `provider-artifacts`' HTTP routes from the browser, and the first to
1084
+ call `resolve`. `hello` contributes none of the six points above via a slot (nor `attention` -- its own manifest
1021
1085
  declares panels/cards/actions/jobs/routes/settings only, matching the intro above) -- it's the
1022
1086
  reference for those six, not for slot provision or the attention rail; `plugins/scans` and
1023
1087
  `plugins/overview` are the worked examples for `contributes.attention` (§8 above).
@@ -175,6 +175,16 @@ export interface SlotRegistry {
175
175
  /** Register this plugin's provider instance for `slot@version`. Throws if that slot already has a
176
176
  * provider (from this plugin or another) -- auth/notify are singletons, never a last-write-wins. */
177
177
  provide(slot: string, version: number, impl: unknown): void;
178
+ /** The provider bound for `slot@version`, cast to `T` (a slot's interface in `slots.ts`, e.g.
179
+ * `ArtifactsProvider`), or `null` when nothing is bound. A plugin may resolve only a slot its
180
+ * manifest lists in `contributes.consumes`; any other throws, naming the plugin and the slot. An
181
+ * installed plugin may not resolve the `auth` slot at all, declared or not (as it may not provide it);
182
+ * the install gate also refuses a manifest that declares `provides` or `consumes` for `auth` (#604),
183
+ * so this call-time refusal is what remains for a plugin staged before that check.
184
+ * Call it at request time (inside a route handler or job), never in `activate()`: providers bind
185
+ * during activation in load order, so a consumer that activates before its provider would read `null`
186
+ * there for good (#97). */
187
+ resolve<T = unknown>(slot: string, version: number): T | null;
178
188
  }
179
189
  /** A logger prefixed with the plugin id. */
180
190
  export interface LogService {
package/dist/slots.d.ts CHANGED
@@ -99,19 +99,60 @@ export interface IssuesProvider {
99
99
  /** A repo's own labels, independent of the standard (E5 #58). */
100
100
  repoLabels(repo: RepoRef): Promise<Label[]>;
101
101
  }
102
- /** A node in the artifacts report tree (v1 seam; E6 #68 enriches). */
102
+ /** A node in the artifacts report tree (v1 seam; E6 #68 enriches).
103
+ *
104
+ * A `report` node's `id` is its markdown source's path relative to the store's `artifacts/` directory
105
+ * (`Code-Reviews/2026-09-01/summary.md`), the id `source`/`read` take; a report with no markdown source
106
+ * (a rendered `.html` nothing in the store produced) falls back to its `href`. A `category` node's id is
107
+ * `category:<name>` and a `run` node's `run:<category>/<run>`; neither names a readable source. */
103
108
  export interface ArtifactNode {
104
109
  id: string;
105
110
  title: string;
106
111
  kind: "category" | "run" | "report";
107
112
  children?: ArtifactNode[];
113
+ /** A report's markdown source path (the same relative path its `id` carries), absent when the report has
114
+ * no markdown source. A node without it is a rendered `.html` only: `source`/`read` then answer that
115
+ * rendered HTML, not markdown, so a consumer that expects markdown must skip such a node. Report nodes
116
+ * only (#97). */
117
+ md?: string;
118
+ /** A report's viewer path: the rendered `.html` once rendered (`provider-artifacts`' `GET /render/<href>`
119
+ * serves it from its docroot), else the `.md` source path, which that route does not serve (it reads the
120
+ * docroot only; read the text through `source`). Report nodes only (#97). */
121
+ href?: string;
122
+ }
123
+ /** One `search` result: a section of one report. */
124
+ export interface ArtifactSearchHit {
125
+ /** The id of the report the hit is in, built the same way as `ArtifactNode.id` for a report that has a
126
+ * markdown source (search only reads markdown), so a hit opens through `source`/`read`. A report the
127
+ * tree leaves out, such as a rendered `index` page, can still surface here. */
128
+ id: string;
129
+ /** The report's title as the search reads it (its first level-1 heading, else its file name with the
130
+ * extension dropped and `-`/`_` shown as spaces). For a rendered report that can differ from the tree
131
+ * node's title, which comes from the rendered page's `<title>`. */
132
+ title: string;
133
+ /** The matching section's heading id inside the rendered report, without a leading `#`. Absent when the
134
+ * report is not rendered, when its rendered heading count disagrees with the source's, or when the
135
+ * section has no id (a hit on the report's preamble, or an empty `id` attribute). */
136
+ anchor?: string;
137
+ /** A short excerpt around the match. */
138
+ snippet: string;
108
139
  }
109
- /** `artifacts@1`: tree/read/search/render-status. */
140
+ /** `artifacts@1`: the report tree, one report's source by id, and ranked search over the markdown
141
+ * sources. A consumer reaches it in-process through `HostApi.slots.resolve("artifacts", 1)` (#97). */
110
142
  export interface ArtifactsProvider {
111
143
  /** The report tree. */
112
144
  tree(): Promise<ArtifactNode[]>;
113
- /** Raw source of one report by id. */
145
+ /** Raw source of one report by id; throws exactly when `source` would answer `null`. */
114
146
  read(id: string): Promise<string>;
147
+ /** Ranked cross-report search over the store's markdown sources, at most 50 hits (the cap `GET /search`
148
+ * has); `CONTEXT.md`, `CONTENT.md`, `Archive/` and `.sync-inbox/` are not searched. An empty array for
149
+ * an empty query. */
150
+ search(q: string): Promise<ArtifactSearchHit[]>;
151
+ /** Raw source of one report by id: `null` when no `.md` or `.html` resolves at that path inside the
152
+ * store (or the path escapes it). The tree, not `source`, says what counts as a report: a path the tree
153
+ * skips (an `index` page whose html exists, `CONTEXT.md`, anything under `Archive/`) still answers its
154
+ * text here. */
155
+ source(id: string): Promise<string | null>;
115
156
  }
116
157
  /** An authenticated identity. */
117
158
  export interface Identity {
package/dist/views.d.ts CHANGED
@@ -14,6 +14,15 @@ export interface PluginPanelView {
14
14
  nav?: string;
15
15
  export: string;
16
16
  navOwner: boolean;
17
+ /**
18
+ * Present (and only ever `"env"` today) on every contestant of a contested nav slot whose choice of owner
19
+ * an `AC_NAV_<SLOT>_PANEL` environment variable fixes (#628), so the shell can render that slot's switcher
20
+ * control disabled with the reason, and a script can expect `409` from `POST /api/nav-slot` for it. The
21
+ * variable pins the slot whatever its value is (one naming no contestant leaves the default owner, and the
22
+ * slot is still pinned). Absent otherwise (never `undefined`-valued). Optional, so a shell built before it
23
+ * ignores it.
24
+ */
25
+ navPinnedBy?: "env";
17
26
  /**
18
27
  * This panel's position among every panel in the response (#472): a non-negative integer, lower
19
28
  * first, resolved by the host per request from the operator's `tab_order` setting, then each
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rackbops/ac-plugin-contract",
3
- "version": "2.0.0-alpha.42",
3
+ "version": "2.0.0-alpha.43",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "The artifact-console plugin ABI for out-of-repo plugin authors: HostApi, the acPlugin manifest contribution points, the generated manifest JSON Schema, and the @ac/host ambient browser declaration.",