@rackbops/ac-plugin-contract 2.0.0-alpha.33

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.
@@ -0,0 +1,365 @@
1
+ /**
2
+ * `HostApi` is the service surface the host passes to `createPlugin(host)` on the server, and the
3
+ * `@ac/host` module the shell exposes to frontend bundles in the browser (design.md §3). This is
4
+ * the v1 shape; the server implementation landed in #28, with `sidecar` real as of #29's proxy,
5
+ * `jobs` as of #31's scheduler, `agent` as of E4, and `github` real as of #127's Octokit client; the
6
+ * browser side lands in #32. Methods are added additively — a breaking change bumps
7
+ * `HOST_API_VERSION`.
8
+ */
9
+ import type { Octokit } from "@octokit/rest";
10
+ import type { HttpMethod } from "./manifest.js";
11
+ import type { AgentCallOutcome, JobMachineRun, MachineView } from "./views.js";
12
+ /** A typed, per-plugin settings accessor (backed by the host's settings overlay). */
13
+ export interface SettingsService {
14
+ get<T = unknown>(key: string): Promise<T | undefined>;
15
+ set(key: string, value: unknown): Promise<void>;
16
+ }
17
+ /** Per-plugin durable storage: a SQLite table scoped to the plugin, with atomic JSON helpers. */
18
+ export interface StorageService {
19
+ get<T = unknown>(key: string): Promise<T | undefined>;
20
+ set(key: string, value: unknown): Promise<void>;
21
+ delete(key: string): Promise<void>;
22
+ }
23
+ /** Register/trigger the plugin's contributed jobs and read their last result. */
24
+ export interface JobsService {
25
+ /** Run a contributed job now (the manual-trigger endpoint). For a `runOn: "agent"` job this resolves
26
+ * once every currently paired machine has either run or been queued. */
27
+ trigger(jobId: string): Promise<void>;
28
+ /** The last run of a job, if it has run — for an agent job, the newest recorded run across every
29
+ * machine. */
30
+ lastRun(jobId: string): Promise<{
31
+ startedAt: string;
32
+ ok: boolean;
33
+ } | undefined>;
34
+ /** One row per relevant machine (#51): a single null-id row for a host job, one row per currently
35
+ * paired machine for an agent job. */
36
+ machineRuns(jobId: string): Promise<JobMachineRun[]>;
37
+ /** #78: the SAME shape as `machineRuns`, for an OTHER plugin's declared job -- `machineRuns` stays
38
+ * scoped to the calling plugin's own jobs; this is the needs-attention rollup's in-process read of
39
+ * every OTHER loaded plugin's job health, so `overview`'s `/attention` route never has to make an
40
+ * HTTP call back into its own host. Discloses nothing `GET /api/jobs/:pluginId/:jobId/runs`
41
+ * doesn't already serve publicly -- just a non-HTTP path to the same data. Optional: only
42
+ * `overview` calls it today. */
43
+ machineRunsOf?(pluginId: string, jobId: string): Promise<JobMachineRun[]>;
44
+ }
45
+ /** #78: every loaded in-process plugin's own declared jobs -- lets the needs-attention rollup
46
+ * discover what to poll via `jobs.machineRunsOf` without a second manifest fetch or an HTTP
47
+ * self-loop. Host-internal discovery only (unlike `PluginView`, never served over HTTP as its own
48
+ * route); optional on `HostApi` since only `overview` uses it. */
49
+ export interface PluginJobsService {
50
+ list(): {
51
+ pluginId: string;
52
+ jobs: JobDeclaration[];
53
+ }[];
54
+ }
55
+ /** The subset of `JobContribution` (`manifest.ts`) the rollup needs -- id, schedule metadata are
56
+ * irrelevant to it, so this stays a narrow projection rather than the full manifest shape. */
57
+ export interface JobDeclaration {
58
+ id: string;
59
+ runOn: "host" | "agent";
60
+ }
61
+ /** The typed event bus (e.g. `issue.changed`, `job.finished`, `artifact.rendered`). */
62
+ export interface EventsService {
63
+ emit(type: string, payload: unknown): void;
64
+ /** Subscribe; returns an unsubscribe function. */
65
+ on(type: string, handler: (payload: unknown) => void): () => void;
66
+ }
67
+ /** One shared, rate-limit-aware GitHub client (Octokit; PAT from env, #127). Consumers make every
68
+ * GitHub call through `octokit()` rather than constructing a second client. */
69
+ export interface GithubService {
70
+ /** The authenticated login, cached. */
71
+ viewer(): Promise<string>;
72
+ /** The one shared Octokit REST client — consumers make all GitHub calls through this. */
73
+ octokit(): Octokit;
74
+ /** Remaining primary-rate-limit budget from the last response's headers, or undefined before any
75
+ * call. */
76
+ rateLimit(): {
77
+ remaining: number;
78
+ limit: number;
79
+ resetAt: string;
80
+ } | undefined;
81
+ }
82
+ /**
83
+ * Call a paired host-agent verb, or list the paired machines (#50). `HostApi.agent` is scoped per
84
+ * plugin the same way `storage`/`settings` are (`host-factory.ts`) -- the plugin id a call carries
85
+ * is the host's own, never a value the plugin supplies, so a plugin can only ever act as itself and
86
+ * cannot reach pairing or grants through this surface at all. Streaming (`GET /v1/jobs/:id/events`)
87
+ * and the `runOn: "agent"` job fan-out are #51/#54.
88
+ */
89
+ export interface AgentService {
90
+ /** The ids of the machines currently paired with the console. */
91
+ machines(): Promise<string[]>;
92
+ /** The paired machines, with their current grants. */
93
+ list(): Promise<MachineView[]>;
94
+ /** Call one of this plugin's declared capabilities on a paired machine's agent. */
95
+ call<T = unknown>(machineId: string, method: "GET" | "POST", path: string, body?: unknown, opts?: {
96
+ /** How long to wait for the agent's response headers (undici's headersTimeout; its default is
97
+ * 300 s). A long-running verb -- a scan -- passes the verb's own limit plus a margin, so the
98
+ * agent's own timeout answer arrives before the console gives up. */
99
+ timeoutMs?: number;
100
+ }): Promise<AgentCallOutcome<T>>;
101
+ /**
102
+ * Open a paired machine's job event stream (`GET /v1/jobs/:jobId/events`, #54) and relay it into a
103
+ * new host task: every agent `line` frame becomes a `task.log`, the `done` frame finishes it --
104
+ * the tasks drawer streams the machine's job with no new UI (decision 12/20). Resolves with the new
105
+ * task's id once the stream is confirmed open (the agent answered `200`); the relay itself then runs
106
+ * detached and can never surface here. Rejects (starting no task) on an invalid `jobId`, an unknown
107
+ * machine, or any non-`200` response from the agent (a `404` unknown job, a `403` refusal, ...) --
108
+ * the same failure shapes `call` throws for a bad path, surfaced the same way.
109
+ */
110
+ streamJob(machineId: string, jobId: string, opts: {
111
+ label: string;
112
+ }): Promise<{
113
+ taskId: string;
114
+ }>;
115
+ }
116
+ /** Proxy to a sidecar plugin's HTTP surface (the host handles health + base URL). */
117
+ export interface SidecarService {
118
+ /** The base URL the host proxies to for a sidecar plugin. */
119
+ baseUrl(pluginId: string): string;
120
+ }
121
+ /** Register a confirm-gated action; the shell builds its button + modal from the manifest data. */
122
+ export interface ActionsService {
123
+ register(action: {
124
+ id: string;
125
+ run: (input: unknown) => Promise<unknown>;
126
+ }): void;
127
+ }
128
+ /**
129
+ * The `name` of the `Error` an action's `run` throws to say the CALLER's input was malformed (a
130
+ * missing or wrong-typed field, a bad `owner/name`, ...) as opposed to something going wrong in the
131
+ * world (a GitHub failure, a store that isn't cloned): the host's action route
132
+ * (`POST /api/actions/:pluginId/:actionId`) answers such an error `400` with its message, and any
133
+ * other thrown error `500`.
134
+ *
135
+ * A NAME, deliberately not a class the host exports for plugins to import: a plugin server imports
136
+ * `@ac/contract` as `import type` only (`docs/plugins/authoring.md` §2 -- its compiled `dist/server.js`
137
+ * carries no runtime imports), and a class here would give every plugin a runtime dependency on it,
138
+ * while `instanceof` cannot see across a rollup-bundled server that inlines its own copy. Each plugin
139
+ * that validates input carries a small local class instead (the `AbortError` idiom), typed against
140
+ * this name so renaming it here fails the plugin's build rather than silently turning its 400s into
141
+ * 500s:
142
+ *
143
+ * ```ts
144
+ * import type { ActionInputErrorName } from "@rackbops/ac-plugin-contract"
145
+ *
146
+ * export class ActionInputError extends Error {
147
+ * override name: ActionInputErrorName = "ActionInputError"
148
+ * }
149
+ * ```
150
+ *
151
+ * Types only: no runtime export, no `HOST_API_VERSION` bump.
152
+ */
153
+ export type ActionInputErrorName = "ActionInputError";
154
+ /**
155
+ * The in-process composition-slot binding (#84): a plugin registers its provider instance for a slot
156
+ * it `contributes.provides` in `activate()`; the host holds the one process-level binding and resolves
157
+ * it for the pipeline (`auth@1`) or another plugin (`notify@1`, #87) to call. Deliberately untyped by
158
+ * slot here (`impl: unknown`) -- the host casts to the slot's real interface (`AuthProvider`,
159
+ * `NotifyProvider`, `slots.ts`) when it resolves, the same way `RoutesService`/`ActionsService` don't
160
+ * know their callers' payload shapes either. Two providers for the same `slot@version` is a boot
161
+ * error (auth/notify are singletons) -- surfaced by the host-side registry `provide` throws from, not
162
+ * this interface, since this is the plugin's OWN scoped view (pre-bound to its id, so the error can
163
+ * name the real offending plugin without a pluginId parameter here). */
164
+ export interface SlotRegistry {
165
+ /** Register this plugin's provider instance for `slot@version`. Throws if that slot already has a
166
+ * provider (from this plugin or another) -- auth/notify are singletons, never a last-write-wins. */
167
+ provide(slot: string, version: number, impl: unknown): void;
168
+ }
169
+ /** A logger prefixed with the plugin id. */
170
+ export interface LogService {
171
+ info(message: string, ...args: unknown[]): void;
172
+ warn(message: string, ...args: unknown[]): void;
173
+ error(message: string, ...args: unknown[]): void;
174
+ }
175
+ /** Read declared env keys only (the manifest's `env[]`). */
176
+ export interface EnvService {
177
+ get(key: string): string | undefined;
178
+ }
179
+ /**
180
+ * Register HTTP handlers for the plugin's declared routes. The host mounts each under the plugin's
181
+ * own namespace `/api/x/<id>/…` (never bare); `handler` is a web-standard `Request` -> `Response`, so
182
+ * a plugin does not depend on the host's server. A plugin may only register a `(method, path)` it
183
+ * declared in its manifest `contributes.routes[]`. (Same-origin/CSRF protection of mutating routes is
184
+ * added across every mutating route by #85.)
185
+ */
186
+ export interface RoutesService {
187
+ register(method: HttpMethod, path: string, handler: (request: Request) => Response | Promise<Response>): void;
188
+ }
189
+ /** The server-side services passed to `createPlugin(host)`. */
190
+ export interface HostApi {
191
+ settings: SettingsService;
192
+ storage: StorageService;
193
+ jobs: JobsService;
194
+ events: EventsService;
195
+ github: GithubService;
196
+ agent: AgentService;
197
+ sidecar: SidecarService;
198
+ routes: RoutesService;
199
+ actions: ActionsService;
200
+ log: LogService;
201
+ env: EnvService;
202
+ /** #78: see `PluginJobsService`. Optional -- only `overview`'s needs-attention rollup uses it. */
203
+ plugins?: PluginJobsService;
204
+ /** #84: see `SlotRegistry`. */
205
+ slots: SlotRegistry;
206
+ }
207
+ /**
208
+ * The context the host passes to `Plugin.runJob` for one job run (#31). `jobId` selects which
209
+ * contributed job to run (one `runJob` dispatches all of the plugin's `contributes.jobs[]`); `signal`
210
+ * aborts when the run exceeds its `timeoutSec`, and a cooperative job should stop when it fires; `log`
211
+ * appends a line to THIS run's recorded log tail. Only lines written to `log` appear in the run
212
+ * record — `host.log` is operational logging and is not captured in the tail. `machine` is present
213
+ * for every run of a `runOn: "agent"` job (one run per paired machine, #51) and absent for a
214
+ * `runOn: "host"` job, which runs once with no machine.
215
+ */
216
+ export interface JobRunContext {
217
+ jobId: string;
218
+ signal: AbortSignal;
219
+ log(line: string): void;
220
+ machine?: {
221
+ id: string;
222
+ name: string;
223
+ };
224
+ }
225
+ /** A plugin instance. `createPlugin(host)` is pure; I/O only in `activate()` (design §4 lifecycle). */
226
+ export interface Plugin {
227
+ activate(): void | Promise<void>;
228
+ /**
229
+ * Run one of the plugin's contributed jobs, dispatched by `ctx.jobId` (#31's scheduler). Optional —
230
+ * a plugin that contributes no jobs omits it; the host records a job as failed if it is declared but
231
+ * no `runJob` is exported. A `runOn: "host"` job runs in-process once; a `runOn: "agent"` job runs
232
+ * in-process too, once per currently paired machine, with `ctx.machine` set (#51) — the agent itself
233
+ * runs no plugin code, so machine-side work goes through `host.agent.call(ctx.machine.id, …)` (#50).
234
+ */
235
+ runJob?(ctx: JobRunContext): void | Promise<void>;
236
+ }
237
+ /** A plugin's server entry: `dist/server.js` exports this as `createPlugin`. */
238
+ export type CreatePlugin = (host: HostApi) => Plugin;
239
+ /** Toast severity, matching notify. */
240
+ export type ToastSeverity = "info" | "warning" | "error";
241
+ /**
242
+ * The browser-side `@ac/host` a frontend bundle imports. v1 (implemented in #32) is `theme` tokens +
243
+ * `toast`; a `fetch` wrapper and `slots` grow additively when a consumer needs them (the issues panel,
244
+ * E5). The E3 chrome-integrated upgrade landed across #38 (toast portal), #41 (modal), and #142
245
+ * (quickcast). Methods are added additively — a breaking change bumps `HOST_API_VERSION`.
246
+ */
247
+ export interface BrowserHostApi {
248
+ /** Show a toast. */
249
+ toast(message: string, opts?: {
250
+ severity?: ToastSeverity;
251
+ }): void;
252
+ /** Resolve a theme token (a `--rb-*` custom property) to its current value. */
253
+ theme: {
254
+ token(name: string): string;
255
+ };
256
+ /**
257
+ * The shell's header button slot (#38): put a button in the header; returns the unregister. Safe to
258
+ * call from a mounted panel/card (an effect) or at bundle import time — a registration made before the
259
+ * shell mounts is held and delivered when it does, and its unregister works in either state.
260
+ */
261
+ header: {
262
+ registerButton(button: HeaderButtonSpec): () => void;
263
+ };
264
+ /**
265
+ * Open the universal confirm modal for a registered action (#41). Fire-and-forget (returns nothing);
266
+ * safe to call before the shell mounts — held and opened once it does, or dropped if it never mounts,
267
+ * the same pending-buffer semantics as `header.registerButton`.
268
+ *
269
+ * `subscribe` (#460) is the completion signal `open` lacks: the listener runs once for every action
270
+ * the modal actually dispatched and got a result for -- success or failure -- whoever opened it (a
271
+ * plugin's `open`, the Ctrl+K palette), so a panel can refresh what an action just changed. A Cancel
272
+ * or Escape never fires it. Returns the unsubscribe. A listener that throws is logged and skipped;
273
+ * it never reaches the modal or another listener.
274
+ */
275
+ actions: {
276
+ open(ref: ActionOpenRef): void;
277
+ subscribe(listener: (event: ActionSettledEvent) => void): () => void;
278
+ };
279
+ /**
280
+ * Register a source of quickcast items for the shell's Ctrl+K palette (#40): the palette calls every
281
+ * registered source on open (and again on each filter pass) and lists the collected items after its
282
+ * own built-in panel/action items, in registration order. The plugin's `run` decides what happens
283
+ * (navigate, open its own UI, call `actions.open`); the palette closes and then calls `run`. Returns
284
+ * the unregister. Safe to call before the shell mounts — held and delivered when it does, the same
285
+ * pending-buffer semantics as `header.registerButton`.
286
+ */
287
+ quickcast: {
288
+ register(source: () => QuickcastItem[]): () => void;
289
+ };
290
+ /**
291
+ * Same-origin fetch to a host/plugin route (E5 #56, 56c). A relative `path` resolves against the
292
+ * console origin; an absolute `path` whose origin differs from the console's own is REJECTED (throws)
293
+ * rather than followed -- `credentials: "same-origin"` alone would not stop the request itself, or a
294
+ * write's `X-AC-CSRF` token, from reaching a third-party origin. Credentials are included
295
+ * (`same-origin`); a non-GET/HEAD/OPTIONS request carries the `X-AC-CSRF` token fetched once from
296
+ * `GET /api/config` (#85) and cached -- defense-in-depth alongside the Origin check #85 already
297
+ * enforces, not itself required yet. Panels read a provider's routes (e.g.
298
+ * `/api/x/provider-github/…`) through this rather than a bare `fetch`. Returns the raw `Response`;
299
+ * the caller parses it.
300
+ */
301
+ fetch(path: string, init?: RequestInit): Promise<Response>;
302
+ /**
303
+ * `fetch`, wrapped in a per-path TTL cache (E7 #77): only `GET` may be cached (throws on any
304
+ * other method); a call within `ttlMs` (default 45 s) of the path's last fetch reuses that SAME
305
+ * underlying request rather than issuing a new one, and each caller gets its own `Response` via
306
+ * `.clone()` -- two independent reads of a cache hit never collide on an already-consumed body.
307
+ * A rejected fetch is dropped immediately, not cached for the rest of the window. A card reads
308
+ * through `cached`, never bare `fetch`, so Refresh (`invalidate`) and the heartbeat reach it.
309
+ */
310
+ cached(path: string, init?: RequestInit, opts?: {
311
+ ttlMs?: number;
312
+ }): Promise<Response>;
313
+ /** Bypass the cache: drop one path's entry, or every entry when `path` is omitted (Refresh). */
314
+ invalidate(path?: string): void;
315
+ }
316
+ /** One entry a plugin contributes to the quickcast palette (#40, #142). */
317
+ export interface QuickcastItem {
318
+ label: string;
319
+ hint?: string;
320
+ run: () => void;
321
+ }
322
+ /** A button a plugin contributes to the shell header (#38). Re-registering an `id` replaces it. */
323
+ export interface HeaderButtonSpec {
324
+ id: string;
325
+ label: string;
326
+ onClick: () => void;
327
+ }
328
+ /**
329
+ * Which registered action to open the universal modal for (#41), and what it operates on. `target`
330
+ * scopes the draft key (e.g. an issue number) when the action is opened against a specific object;
331
+ * omitted for a `scope: "global"` action, which falls back to the action's own `scope` as the key.
332
+ */
333
+ export interface ActionOpenRef {
334
+ pluginId: string;
335
+ actionId: string;
336
+ target?: string;
337
+ /** Initial field values for this open (#71a). An explicit seed WINS over a stored draft for the same
338
+ * scope key -- for the fields it NAMES (#478): every other field keeps the draft's value, else its
339
+ * default, so text typed into the form survives a cancel and a seeded reopen. The merged values are
340
+ * written as the draft at once, so a reload keeps exactly what the form shows. A field the seed names as
341
+ * empty (`""`, `[]`, `0`, `false`) still wins -- so seed only what you know, and omit a key rather than
342
+ * seed `""` for "unknown" -- and only an absent key (or `undefined`) leaves it to the draft. Absent
343
+ * `input` -> the draft's values for the action's own fields, then the field defaults. A key not among the action's
344
+ * declared `fields` is dropped, from the seed and from the draft, before it ever reaches the form or
345
+ * the submitted input, so a stale/mistargeted seed can never post a stray field. A seed must name every
346
+ * field whose meaning depends on another field it names (`originalLabels` with `labels`): the one it
347
+ * leaves out keeps a possibly stale drafted value. */
348
+ input?: Record<string, unknown>;
349
+ }
350
+ /**
351
+ * What `actions.subscribe` delivers (#460): one action the universal modal dispatched and got a
352
+ * result for. `target` is the opened ref's own `target` (absent when the opener passed none -- the
353
+ * Ctrl+K palette carries none, and a `scope: "global"` action is usually opened without one) -- what it
354
+ * was opened FOR, not what the operator finally typed into the form. `error` is the failure message
355
+ * (the server's `error` text, or a client-side one such as `http 500`, a network error or a timeout).
356
+ * The event carries no input -- a listener can refetch what changed but never read what was sent.
357
+ */
358
+ export interface ActionSettledEvent {
359
+ pluginId: string;
360
+ actionId: string;
361
+ target?: string;
362
+ ok: boolean;
363
+ error?: string;
364
+ }
365
+ //# sourceMappingURL=host-api.d.ts.map
@@ -0,0 +1,10 @@
1
+ /**
2
+ * `HostApi` is the service surface the host passes to `createPlugin(host)` on the server, and the
3
+ * `@ac/host` module the shell exposes to frontend bundles in the browser (design.md §3). This is
4
+ * the v1 shape; the server implementation landed in #28, with `sidecar` real as of #29's proxy,
5
+ * `jobs` as of #31's scheduler, `agent` as of E4, and `github` real as of #127's Octokit client; the
6
+ * browser side lands in #32. Methods are added additively — a breaking change bumps
7
+ * `HOST_API_VERSION`.
8
+ */
9
+ export {};
10
+ //# sourceMappingURL=host-api.js.map
@@ -0,0 +1,36 @@
1
+ import type { EnvVar } from "./manifest.js";
2
+ /**
3
+ * The plugin-index envelope (#35) — the `plugins.json` shape a host publishes so others can discover its
4
+ * plugins, shared with the discord-bot index (design §4): the same `schemaVersion`/`generatedAt` header
5
+ * and the per-plugin fields `name`, `package`, `version`, `hostApiVersion`, `env[]`, `releases[]`, plus a
6
+ * `host` discriminator so one repo could serve both hosts. `contributes` is deliberately NOT here — it is
7
+ * private to each host. These are pure types; they are not part of the manifest JSON Schema (`gen-schema`
8
+ * generates only from `AcPluginManifest`), so adding this file does not change `schema.json`.
9
+ */
10
+ /** One published release of a plugin, derived from its CHANGELOG. */
11
+ export interface Release {
12
+ version: string;
13
+ /** ISO date of the release, from the CHANGELOG section. */
14
+ date: string;
15
+ /** The CHANGELOG section body, if any. */
16
+ notes?: string;
17
+ }
18
+ /** One plugin's entry in an index. `name` is the shared identifier (AC2 sources it from the manifest `id`). */
19
+ export interface PluginIndexEntry {
20
+ /** Which host this plugin targets, e.g. "artifact-console" — the discriminator on a shared index. */
21
+ host: string;
22
+ name: string;
23
+ /** The npm/package name (package.json `name`). */
24
+ package: string;
25
+ version: string;
26
+ hostApiVersion: number;
27
+ env: EnvVar[];
28
+ releases: Release[];
29
+ }
30
+ /** A `plugins.json` index. `generatedAt` is stamped by a publishing registry; a workspace drift-copy omits it. */
31
+ export interface PluginIndex {
32
+ schemaVersion: number;
33
+ generatedAt?: string;
34
+ plugins: PluginIndexEntry[];
35
+ }
36
+ //# sourceMappingURL=index-envelope.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=index-envelope.js.map
@@ -0,0 +1,16 @@
1
+ /**
2
+ * `@ac/contract` — the plugin ABI: the `acPlugin` manifest + contribution points, the `HostApi`
3
+ * services, the four composition slots, and the version constants. The manifest's JSON Schema is
4
+ * generated from these types by `gen-schema.mjs` into `schema.json`.
5
+ */
6
+ export type * from "./action-options.js";
7
+ export type * from "./attention.js";
8
+ export type * from "./host-api.js";
9
+ export type * from "./index-envelope.js";
10
+ export * from "./issue-filter.js";
11
+ export type * from "./manifest.js";
12
+ export * from "./ov-widgets.js";
13
+ export type * from "./slots.js";
14
+ export { HOST_API_VERSION, SLOTS } from "./version.js";
15
+ export type * from "./views.js";
16
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * `@ac/contract` — the plugin ABI: the `acPlugin` manifest + contribution points, the `HostApi`
3
+ * services, the four composition slots, and the version constants. The manifest's JSON Schema is
4
+ * generated from these types by `gen-schema.mjs` into `schema.json`.
5
+ */
6
+ export * from "./issue-filter.js";
7
+ export * from "./ov-widgets.js";
8
+ export { HOST_API_VERSION, SLOTS } from "./version.js";
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,122 @@
1
+ import type { IssueRecord } from "./slots.js";
2
+ /**
3
+ * The shared filter model (E5 #56, 56c, plan of record decision 4): the eight-axis filter types + a
4
+ * pure `matchesIssue` predicate over the `IssueRecord` shape 56a shipped. Both issue panels — Board
5
+ * (#63) and Classic (#59) — apply this SAME predicate, so their counts agree by construction; neither
6
+ * panel depends on the other, and the predicate lives here (the shared/provider layer) rather than in
7
+ * either. `now` is always injected, never read from `Date.now()` internally, so a caller's tests (and
8
+ * this module's own) are deterministic.
9
+ */
10
+ export type FilterOp = "is" | "is-not";
11
+ /**
12
+ * A set-membership test: `is` matches when the record's value is in `values`; `is-not` when it is not.
13
+ * A value is a plain exact match (a label name, a login) UNLESS it is one of the tokens below (#464a):
14
+ * `@me`, `@unassigned`, `@bots`, `@any`, `@upto:<label>` -- which the predicate resolves against the record
15
+ * and a `MatchContext`. Any OTHER value, including one that merely starts with `@` (a label may be named
16
+ * `@needs-review`), is still the plain exact match it always was.
17
+ */
18
+ export interface SetFilter {
19
+ op: FilterOp;
20
+ values: string[];
21
+ }
22
+ /** The viewer: an assignee/author login equal to `MatchContext.me`, compared ignoring case. */
23
+ export declare const FILTER_ME = "@me";
24
+ /** Nobody: on `assignee`, a record with no assignee; on `author`, a record with no author. */
25
+ export declare const FILTER_UNASSIGNED = "@unassigned";
26
+ /** A bot: an assignee/author whose `Person.isBot` is true -- GitHub's own flag, never a guess from the login. */
27
+ export declare const FILTER_BOTS = "@bots";
28
+ /** Any label of the axis's ladder (`priority`/`effort`/`type`). `is-not @any` is 1.x's "none". */
29
+ export declare const FILTER_ANY = "@any";
30
+ /**
31
+ * "At or before this rung of the ladder" (`priority`/`effort`): the record carries the label named
32
+ * `name` or any label the axis's ladder lists BEFORE it -- "High and above" on the priority ladder
33
+ * (critical first), "S and below" on the effort ladder (XS first): one rule, two wordings. `name` is the
34
+ * full label name, as the ladder carries it.
35
+ */
36
+ export declare function filterUpTo(name: string): string;
37
+ /** A filter value read as a token; `unknown` is an `@...` that is none of the tokens this version defines --
38
+ * the predicate compares it as a plain value (so it matches nothing unless a label/login is spelled so). */
39
+ export type FilterToken = {
40
+ kind: "me" | "unassigned" | "bots" | "any";
41
+ } | {
42
+ kind: "upto";
43
+ name: string;
44
+ } | {
45
+ kind: "unknown";
46
+ value: string;
47
+ };
48
+ /**
49
+ * A value as a token, or `undefined` for a plain value (one not starting with `@`). For the UIs' chip
50
+ * labels; the predicate reads values through it too, so what a chip says and what a filter does cannot
51
+ * disagree about what is a token. An `@...` that is not one of the five tokens comes back as `unknown`,
52
+ * which the predicate treats as a plain value.
53
+ */
54
+ export declare function parseFilterToken(value: string): FilterToken | undefined;
55
+ /**
56
+ * What a token needs from outside the record (#464a): the viewer's login for `@me`, and each ladder axis's
57
+ * label names in the standard's own order (critical first, XS first) for `@any` and `@upto:`. Every field
58
+ * is optional: a token that cannot be resolved without one it was not given matches nothing. Passed as
59
+ * `matchesIssue`'s optional fourth argument, so every three-argument caller behaves exactly as before.
60
+ */
61
+ export interface MatchContext {
62
+ me?: string;
63
+ ladders?: {
64
+ priority?: string[];
65
+ effort?: string[];
66
+ type?: string[];
67
+ };
68
+ }
69
+ /** An age/activity bucket, measured from `now` against a record's `createdAt`/`updatedAt`. */
70
+ export type AgeBucket = "1d" | "1w" | "1m" | "3m" | "older";
71
+ export interface AgeFilter {
72
+ op: FilterOp;
73
+ buckets: AgeBucket[];
74
+ }
75
+ /**
76
+ * Every axis is optional; an absent axis imposes no constraint. `priority`/`effort`/`type`/`tags` are
77
+ * all "the record has a label whose name is in the set" -- the UI groups labels into these ladders
78
+ * (from `labels.json`, #58), but for a plain value the predicate treats all four identically: a plain
79
+ * label-name test. A token (#464a) adds `@any` and `@upto:<rung>` on the ladder axes, resolved against
80
+ * `MatchContext.ladders`; `tags` has no ladder, so a token there matches nothing.
81
+ */
82
+ export interface IssueFilter {
83
+ priority?: SetFilter;
84
+ effort?: SetFilter;
85
+ type?: SetFilter;
86
+ tags?: SetFilter;
87
+ /** Over `assignees[].login`; a record with no assignees matches `is-not` of any set of plain logins, never
88
+ * `is` -- `@unassigned` is the token that matches exactly that record, `@me`/`@bots` the viewer/bots. */
89
+ assignee?: SetFilter;
90
+ /** Over the single `author?.login`; `undefined` never matches `is` of a plain login, always matches
91
+ * `is-not` (`@unassigned` matches a record with no author; `@me`/`@bots` as for `assignee`). */
92
+ author?: SetFilter;
93
+ /** Bucket of `now - createdAt`. */
94
+ age?: AgeFilter;
95
+ /** Bucket of `now - updatedAt`. */
96
+ activity?: AgeFilter;
97
+ /** Title substring (case-insensitive) OR an exact `#<number>` match (a leading `#` is stripped). */
98
+ search?: string;
99
+ }
100
+ /** Classic's list sort order (#59b). Lives here (not in the Classic plugin) so a `SavedView`'s
101
+ * `sort` field and the panel's own sort state share ONE type instead of two independently-typed
102
+ * string unions that could drift. */
103
+ export type SortMode = "opened" | "priority" | "updated";
104
+ /**
105
+ * A saved view (#60): a named filter+sort bundle a picker applies in one click, replacing the whole
106
+ * filter state (never merging into it, per 1.x's own documented decision -- a leftover axis would
107
+ * make a view mean something different depending on what preceded it). Config, not localStorage:
108
+ * `views.yaml` (the issues-classic plugin's own loader) ships the 1.x five as defaults and is the
109
+ * source of truth once an operator edits the file.
110
+ */
111
+ export interface SavedView {
112
+ name: string;
113
+ filter: IssueFilter;
114
+ sort: SortMode;
115
+ }
116
+ /**
117
+ * Pure AND across every present axis. An empty `filter` (`{}`) matches every record. `ctx` (#464a) is what
118
+ * the filter's tokens resolve against (the viewer, the ladders); omit it and every plain value behaves
119
+ * exactly as it always did, while a token that needs `ctx` matches nothing.
120
+ */
121
+ export declare function matchesIssue(record: IssueRecord, filter: IssueFilter, now: number, ctx?: MatchContext): boolean;
122
+ //# sourceMappingURL=issue-filter.d.ts.map