@monosmith/plugin-sdk 0.0.0-stage → 0.2.0

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.
Files changed (164) hide show
  1. package/README.md +90 -2
  2. package/build-dir/build-dir.algorithm.d.ts +16 -0
  3. package/build-dir/build-dir.algorithm.js +26 -0
  4. package/build-dir/index.d.ts +1 -0
  5. package/build-dir/index.js +1 -0
  6. package/ci/ci-contribution.algorithm.d.ts +28 -0
  7. package/ci/ci-contribution.algorithm.js +19 -0
  8. package/ci/index.d.ts +2 -0
  9. package/ci/index.js +1 -0
  10. package/codec/encode-by-schema.algorithm.d.ts +12 -0
  11. package/codec/encode-by-schema.algorithm.js +105 -0
  12. package/codec/encode-error.error.d.ts +4 -0
  13. package/codec/encode-error.error.js +7 -0
  14. package/codec/encode-json-value.algorithm.d.ts +13 -0
  15. package/codec/encode-json-value.algorithm.js +51 -0
  16. package/codec/index.d.ts +6 -0
  17. package/codec/index.js +5 -0
  18. package/codec/message-parse.error.d.ts +4 -0
  19. package/codec/message-parse.error.js +7 -0
  20. package/codec/method-codec.algorithm.d.ts +8 -0
  21. package/codec/method-codec.algorithm.js +31 -0
  22. package/codec/normalize-by-schema.algorithm.d.ts +14 -0
  23. package/codec/normalize-by-schema.algorithm.js +103 -0
  24. package/codec/params-error.error.d.ts +4 -0
  25. package/codec/params-error.error.js +7 -0
  26. package/codec/schema-lookup.algorithm.d.ts +13 -0
  27. package/codec/schema-lookup.algorithm.js +56 -0
  28. package/codec/wire-message.mapper.d.ts +27 -0
  29. package/codec/wire-message.mapper.js +62 -0
  30. package/concurrency/concurrency.policy.d.ts +21 -0
  31. package/concurrency/concurrency.policy.js +30 -0
  32. package/concurrency/index.d.ts +1 -0
  33. package/concurrency/index.js +1 -0
  34. package/conformance/check-generation.use-case.d.ts +12 -0
  35. package/conformance/check-generation.use-case.js +91 -0
  36. package/conformance/check-plugin.use-case.d.ts +25 -0
  37. package/conformance/check-plugin.use-case.js +81 -0
  38. package/conformance/check-scheduling.use-case.d.ts +11 -0
  39. package/conformance/check-scheduling.use-case.js +98 -0
  40. package/conformance/conformance-report.model.d.ts +22 -0
  41. package/conformance/conformance-report.model.js +23 -0
  42. package/conformance/find-manifests.algorithm.d.ts +7 -0
  43. package/conformance/find-manifests.algorithm.js +34 -0
  44. package/conformance/go-list.mapper.d.ts +4 -0
  45. package/conformance/go-list.mapper.js +8 -0
  46. package/conformance/index.d.ts +5 -0
  47. package/conformance/index.js +3 -0
  48. package/framing/frame-message.algorithm.d.ts +5 -0
  49. package/framing/frame-message.algorithm.js +8 -0
  50. package/framing/frame-reader.store.d.ts +15 -0
  51. package/framing/frame-reader.store.js +59 -0
  52. package/framing/framing.error.d.ts +4 -0
  53. package/framing/framing.error.js +7 -0
  54. package/framing/index.d.ts +3 -0
  55. package/framing/index.js +3 -0
  56. package/host/call-interrupted.error.d.ts +16 -0
  57. package/host/call-interrupted.error.js +20 -0
  58. package/host/connect-plugin.use-case.d.ts +26 -0
  59. package/host/connect-plugin.use-case.js +44 -0
  60. package/host/host-options.contract.d.ts +34 -0
  61. package/host/host-options.contract.js +4 -0
  62. package/host/index.d.ts +9 -0
  63. package/host/index.js +5 -0
  64. package/host/plugin-client.client.d.ts +61 -0
  65. package/host/plugin-client.client.js +297 -0
  66. package/host/start-plugin.use-case.d.ts +10 -0
  67. package/host/start-plugin.use-case.js +57 -0
  68. package/index.d.ts +16 -0
  69. package/index.js +21 -0
  70. package/jsondoc/indent-raw-json.algorithm.d.ts +8 -0
  71. package/jsondoc/indent-raw-json.algorithm.js +60 -0
  72. package/jsondoc/index.d.ts +2 -0
  73. package/jsondoc/index.js +2 -0
  74. package/jsondoc/json-document.error.d.ts +4 -0
  75. package/jsondoc/json-document.error.js +7 -0
  76. package/jsondoc/json-document.model.d.ts +38 -0
  77. package/jsondoc/json-document.model.js +113 -0
  78. package/jsondoc/scan-json-object.algorithm.d.ts +17 -0
  79. package/jsondoc/scan-json-object.algorithm.js +132 -0
  80. package/package.json +34 -4
  81. package/plugin-refs/index.d.ts +1 -0
  82. package/plugin-refs/index.js +1 -0
  83. package/plugin-refs/plugin-ref.policy.d.ts +9 -0
  84. package/plugin-refs/plugin-ref.policy.js +22 -0
  85. package/plugin-roles/index.d.ts +1 -0
  86. package/plugin-roles/index.js +1 -0
  87. package/plugin-roles/plugin-role.policy.d.ts +16 -0
  88. package/plugin-roles/plugin-role.policy.js +30 -0
  89. package/projects/audit-settings.policy.d.ts +5 -0
  90. package/projects/audit-settings.policy.js +8 -0
  91. package/projects/index.d.ts +6 -0
  92. package/projects/index.js +6 -0
  93. package/projects/manifest-match.algorithm.d.ts +7 -0
  94. package/projects/manifest-match.algorithm.js +38 -0
  95. package/projects/project-kind.policy.d.ts +9 -0
  96. package/projects/project-kind.policy.js +28 -0
  97. package/projects/publishes.policy.d.ts +7 -0
  98. package/projects/publishes.policy.js +8 -0
  99. package/projects/set-toolchain.algorithm.d.ts +7 -0
  100. package/projects/set-toolchain.algorithm.js +13 -0
  101. package/projects/stack-plugins.algorithm.d.ts +3 -0
  102. package/projects/stack-plugins.algorithm.js +4 -0
  103. package/protocol/index.d.ts +4 -0
  104. package/protocol/index.js +3 -0
  105. package/protocol/protocol-schema.contract.d.ts +3 -0
  106. package/protocol/protocol-schema.contract.js +1976 -0
  107. package/protocol/protocol-types.contract.d.ts +1057 -0
  108. package/protocol/protocol-types.contract.js +43 -0
  109. package/protocol/rpc-error.error.d.ts +9 -0
  110. package/protocol/rpc-error.error.js +13 -0
  111. package/protocol/schema-document.contract.d.ts +58 -0
  112. package/protocol/schema-document.contract.js +1 -0
  113. package/scaffold/index.d.ts +3 -0
  114. package/scaffold/index.js +2 -0
  115. package/scaffold/scaffold-data.mapper.d.ts +15 -0
  116. package/scaffold/scaffold-data.mapper.js +24 -0
  117. package/scaffold/scaffold-project.use-case.d.ts +20 -0
  118. package/scaffold/scaffold-project.use-case.js +13 -0
  119. package/serve/capabilities-of.algorithm.d.ts +14 -0
  120. package/serve/capabilities-of.algorithm.js +31 -0
  121. package/serve/claim-stdout.use-case.d.ts +10 -0
  122. package/serve/claim-stdout.use-case.js +14 -0
  123. package/serve/dispatch-request.use-case.d.ts +15 -0
  124. package/serve/dispatch-request.use-case.js +57 -0
  125. package/serve/index.d.ts +4 -0
  126. package/serve/index.js +2 -0
  127. package/serve/initialize-plugin.use-case.d.ts +9 -0
  128. package/serve/initialize-plugin.use-case.js +16 -0
  129. package/serve/plugin-members.config.d.ts +16 -0
  130. package/serve/plugin-members.config.js +25 -0
  131. package/serve/plugin.contract.d.ts +80 -0
  132. package/serve/plugin.contract.js +1 -0
  133. package/serve/protocol-streams.contract.d.ts +13 -0
  134. package/serve/protocol-streams.contract.js +1 -0
  135. package/serve/request-outcome.contract.d.ts +9 -0
  136. package/serve/request-outcome.contract.js +1 -0
  137. package/serve/request-pool.store.d.ts +49 -0
  138. package/serve/request-pool.store.js +110 -0
  139. package/serve/serve.use-case.d.ts +31 -0
  140. package/serve/serve.use-case.js +180 -0
  141. package/tasks/contribute-tasks.algorithm.d.ts +15 -0
  142. package/tasks/contribute-tasks.algorithm.js +22 -0
  143. package/tasks/index.d.ts +2 -0
  144. package/tasks/index.js +2 -0
  145. package/tasks/task.algorithm.d.ts +18 -0
  146. package/tasks/task.algorithm.js +23 -0
  147. package/templating/file-exists.error.d.ts +6 -0
  148. package/templating/file-exists.error.js +10 -0
  149. package/templating/index.d.ts +4 -0
  150. package/templating/index.js +4 -0
  151. package/templating/parse-template.algorithm.d.ts +11 -0
  152. package/templating/parse-template.algorithm.js +197 -0
  153. package/templating/render-template.algorithm.d.ts +7 -0
  154. package/templating/render-template.algorithm.js +87 -0
  155. package/templating/template-functions.algorithm.d.ts +10 -0
  156. package/templating/template-functions.algorithm.js +104 -0
  157. package/templating/template-syntax.contract.d.ts +33 -0
  158. package/templating/template-syntax.contract.js +1 -0
  159. package/templating/template.error.d.ts +4 -0
  160. package/templating/template.error.js +7 -0
  161. package/templating/tokenize-action.algorithm.d.ts +20 -0
  162. package/templating/tokenize-action.algorithm.js +57 -0
  163. package/templating/write-templates.use-case.d.ts +11 -0
  164. package/templating/write-templates.use-case.js +44 -0
@@ -0,0 +1,56 @@
1
+ /** Follows `$ref` to the definition it names. */
2
+ export function resolveNode(schema, node) {
3
+ let current = node;
4
+ for (let hops = 0; current.$ref !== undefined; hops++) {
5
+ const name = current.$ref.slice('#/$defs/'.length);
6
+ const target = schema.$defs[name];
7
+ if (target === undefined || hops > 16)
8
+ throw new Error(`the schema has no definition ${current.$ref}`);
9
+ current = target;
10
+ }
11
+ return current;
12
+ }
13
+ /** Whether `null` is a value of the node: `["integer", "null"]`, or an `anyOf` with null. */
14
+ export function isNullable(node) {
15
+ return Array.isArray(node.type) ? node.type.includes('null') : node.anyOf?.some(branch => branch.type === 'null') ?? false;
16
+ }
17
+ /** The node without its `null` alternative. */
18
+ export function withoutNull(schema, node) {
19
+ if (Array.isArray(node.type)) {
20
+ const types = node.type.filter(t => t !== 'null');
21
+ return { ...node, type: types.length === 1 ? types[0] : types };
22
+ }
23
+ const branches = node.anyOf?.filter(branch => branch.type !== 'null');
24
+ return branches !== undefined && branches.length === 1 ? resolveNode(schema, branches[0]) : node;
25
+ }
26
+ /** The kind of value a node describes, after `$ref` and null are set aside. */
27
+ export function kindOf(schema, node) {
28
+ const base = resolveNode(schema, withoutNull(schema, node));
29
+ switch (base.type) {
30
+ case 'string': {
31
+ return 'string';
32
+ }
33
+ case 'integer': {
34
+ return 'integer';
35
+ }
36
+ case 'number': {
37
+ return 'number';
38
+ }
39
+ case 'boolean': {
40
+ return 'boolean';
41
+ }
42
+ case 'array': {
43
+ return 'array';
44
+ }
45
+ case 'object': {
46
+ return base.properties === undefined ? 'map' : 'struct';
47
+ }
48
+ default: {
49
+ return 'any';
50
+ }
51
+ }
52
+ }
53
+ /** The node as the codec walks it: nullability removed, references followed. */
54
+ export function baseNode(schema, node) {
55
+ return resolveNode(schema, withoutNull(schema, node));
56
+ }
@@ -0,0 +1,27 @@
1
+ /** A request or notification as it was received. `id` is `null` for a notification. */
2
+ export interface ReceivedRequest {
3
+ id: number | null;
4
+ method: string;
5
+ params: unknown;
6
+ }
7
+ /** A response as it was received. */
8
+ export interface ReceivedResponse {
9
+ id: number | null;
10
+ result: unknown;
11
+ error: {
12
+ code: number;
13
+ message: string;
14
+ } | null;
15
+ }
16
+ /** A request as the Go SDK writes it: `jsonrpc`, `id`, `method`, `params`, the params left out when there are none. */
17
+ export declare function writeRequest(id: number, method: string, paramsJson: string | undefined): string;
18
+ /** A notification as the Go SDK writes it: a request with no `id`, which gets no response. */
19
+ export declare function writeNotification(method: string, paramsJson: string): string;
20
+ /** A successful response:`id` is `null` only when the request could not be read. */
21
+ export declare function writeResult(id: number | null, resultJson: string): string;
22
+ /** An error response. */
23
+ export declare function writeFailure(id: number | null, code: number, message: string): string;
24
+ /** Reads a request frame. A `null` or missing id makes it a notification. */
25
+ export declare function parseRequest(body: string): ReceivedRequest;
26
+ /** Reads a response frame. */
27
+ export declare function parseResponse(body: string): ReceivedResponse;
@@ -0,0 +1,62 @@
1
+ import { encodeJsonString } from './encode-json-value.algorithm.js';
2
+ import { MessageParseError } from './message-parse.error.js';
3
+ /** A request as the Go SDK writes it: `jsonrpc`, `id`, `method`, `params`, the params left out when there are none. */
4
+ export function writeRequest(id, method, paramsJson) {
5
+ const params = paramsJson === undefined ? '' : `,"params":${paramsJson}`;
6
+ return `{"jsonrpc":"2.0","id":${id},"method":${encodeJsonString(method)}${params}}`;
7
+ }
8
+ /** A notification as the Go SDK writes it: a request with no `id`, which gets no response. */
9
+ export function writeNotification(method, paramsJson) {
10
+ return `{"jsonrpc":"2.0","method":${encodeJsonString(method)},"params":${paramsJson}}`;
11
+ }
12
+ /** A successful response:`id` is `null` only when the request could not be read. */
13
+ export function writeResult(id, resultJson) {
14
+ return `{"jsonrpc":"2.0","id":${id === null ? 'null' : id},"result":${resultJson}}`;
15
+ }
16
+ /** An error response. */
17
+ export function writeFailure(id, code, message) {
18
+ return `{"jsonrpc":"2.0","id":${id === null ? 'null' : id},"error":{"code":${code},"message":${encodeJsonString(message)}}}`;
19
+ }
20
+ function parseObject(body) {
21
+ let value;
22
+ try {
23
+ value = JSON.parse(body);
24
+ }
25
+ catch (error) {
26
+ throw new MessageParseError(error instanceof Error ? error.message : String(error));
27
+ }
28
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
29
+ throw new MessageParseError('a message is a JSON object');
30
+ return value;
31
+ }
32
+ function readId(message) {
33
+ const id = message.id;
34
+ if (id === undefined || id === null)
35
+ return null;
36
+ if (typeof id !== 'number' || !Number.isSafeInteger(id))
37
+ throw new MessageParseError('id must be an integer');
38
+ return id;
39
+ }
40
+ /** Reads a request frame. A `null` or missing id makes it a notification. */
41
+ export function parseRequest(body) {
42
+ const message = parseObject(body);
43
+ const id = readId(message);
44
+ const method = message.method ?? '';
45
+ if (typeof method !== 'string')
46
+ throw new MessageParseError('method must be a string');
47
+ return { id, method, params: message.params };
48
+ }
49
+ /** Reads a response frame. */
50
+ export function parseResponse(body) {
51
+ const message = parseObject(body);
52
+ const id = readId(message);
53
+ const error = message.error;
54
+ if (error === undefined || error === null)
55
+ return { id, result: message.result, error: null };
56
+ if (typeof error !== 'object' || Array.isArray(error))
57
+ throw new MessageParseError('error must be an object');
58
+ const { code, message: text } = error;
59
+ if (typeof code !== 'number' || typeof text !== 'string')
60
+ throw new MessageParseError('error needs a numeric code and a message');
61
+ return { id, result: undefined, error: { code, message: text } };
62
+ }
@@ -0,0 +1,21 @@
1
+ /** The `concurrency` value that leaves the number of simultaneous requests to the host. */
2
+ export declare const CONCURRENCY_UNLIMITED = -1;
3
+ /** How many requests the host keeps in flight to a plugin that declared unlimited concurrency. */
4
+ export declare const UNLIMITED_IN_FLIGHT = 32;
5
+ /** The most workers a plugin that declared unlimited concurrency runs: the host decides, but the pool stays bounded. */
6
+ export declare const UNLIMITED_WORKERS = 1024;
7
+ /** Whether a declared concurrency is one the protocol defines: absent (0), a count (1 and above) or unlimited (-1). */
8
+ export declare function isValidConcurrency(declared: number): boolean;
9
+ /**
10
+ * Reads a declared concurrency: how many requests may be in flight at once,
11
+ * or `unlimited`. An absent, zero or invalid value is 1, the default of every
12
+ * plugin that says nothing.
13
+ */
14
+ export declare function concurrencyLimit(declared: number | undefined): {
15
+ limit: number;
16
+ unlimited: boolean;
17
+ };
18
+ /** How many requests the host keeps in flight to a plugin that declared `declared`. */
19
+ export declare function inFlightLimit(declared: number | undefined): number;
20
+ /** How many requests a plugin that declared `declared` works on at once (its pool size). */
21
+ export declare function workerLimit(declared: number | undefined): number;
@@ -0,0 +1,30 @@
1
+ /** The `concurrency` value that leaves the number of simultaneous requests to the host. */
2
+ export const CONCURRENCY_UNLIMITED = -1;
3
+ /** How many requests the host keeps in flight to a plugin that declared unlimited concurrency. */
4
+ export const UNLIMITED_IN_FLIGHT = 32;
5
+ /** The most workers a plugin that declared unlimited concurrency runs: the host decides, but the pool stays bounded. */
6
+ export const UNLIMITED_WORKERS = 1024;
7
+ /** Whether a declared concurrency is one the protocol defines: absent (0), a count (1 and above) or unlimited (-1). */
8
+ export function isValidConcurrency(declared) {
9
+ return Number.isSafeInteger(declared) && declared >= CONCURRENCY_UNLIMITED;
10
+ }
11
+ /**
12
+ * Reads a declared concurrency: how many requests may be in flight at once,
13
+ * or `unlimited`. An absent, zero or invalid value is 1, the default of every
14
+ * plugin that says nothing.
15
+ */
16
+ export function concurrencyLimit(declared) {
17
+ if (declared === CONCURRENCY_UNLIMITED)
18
+ return { limit: 0, unlimited: true };
19
+ return { limit: declared === undefined || !Number.isSafeInteger(declared) || declared < 1 ? 1 : declared, unlimited: false };
20
+ }
21
+ /** How many requests the host keeps in flight to a plugin that declared `declared`. */
22
+ export function inFlightLimit(declared) {
23
+ const { limit, unlimited } = concurrencyLimit(declared);
24
+ return unlimited ? UNLIMITED_IN_FLIGHT : limit;
25
+ }
26
+ /** How many requests a plugin that declared `declared` works on at once (its pool size). */
27
+ export function workerLimit(declared) {
28
+ const { limit, unlimited } = concurrencyLimit(declared);
29
+ return unlimited ? UNLIMITED_WORKERS : limit;
30
+ }
@@ -0,0 +1 @@
1
+ export { CONCURRENCY_UNLIMITED, UNLIMITED_IN_FLIGHT, UNLIMITED_WORKERS, concurrencyLimit, inFlightLimit, isValidConcurrency, workerLimit } from './concurrency.policy.ts';
@@ -0,0 +1 @@
1
+ export { CONCURRENCY_UNLIMITED, UNLIMITED_IN_FLIGHT, UNLIMITED_WORKERS, concurrencyLimit, inFlightLimit, isValidConcurrency, workerLimit } from './concurrency.policy.js';
@@ -0,0 +1,12 @@
1
+ import type { PluginClient } from '../host/index.ts';
2
+ import type { ConformanceReport } from './conformance-report.model.ts';
3
+ /** The stack name generated projects are asked to record. */
4
+ export declare const CONFORMANCE_STACK = "conformance";
5
+ /**
6
+ * Asks the plugin for one project of each kind, checks the files it reports
7
+ * exist, and, for a language plugin, that its own `discover` returns what it
8
+ * made, with the kind and the stack it was asked for, tasks with commands, and
9
+ * that the dry runs of `version/write`, `remove`, `publish` and `published`
10
+ * answer.
11
+ */
12
+ export declare function checkGeneration(client: PluginClient, workspace: string, report: ConformanceReport): Promise<void>;
@@ -0,0 +1,91 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { effectiveRoles } from '../plugin-roles/index.js';
4
+ import { projectKindDirectory } from '../projects/index.js';
5
+ import { findManifests } from './find-manifests.algorithm.js';
6
+ import { goList, goQuote } from './go-list.mapper.js';
7
+ /** The stack name generated projects are asked to record. */
8
+ export const CONFORMANCE_STACK = 'conformance';
9
+ const LONG_MS = 20 * 60 * 1000;
10
+ function article(kind) {
11
+ return kind === 'app' ? 'an app' : `a ${kind}`;
12
+ }
13
+ /**
14
+ * Asks the plugin for one project of each kind, checks the files it reports
15
+ * exist, and, for a language plugin, that its own `discover` returns what it
16
+ * made, with the kind and the stack it was asked for, tasks with commands, and
17
+ * that the dry runs of `version/write`, `remove`, `publish` and `published`
18
+ * answer.
19
+ */
20
+ export async function checkGeneration(client, workspace, report) {
21
+ const language = effectiveRoles(client.info).includes('language');
22
+ const made = [];
23
+ for (const kind of ['app', 'lib', 'package']) {
24
+ const name = `c${kind}`;
25
+ const params = {
26
+ kind,
27
+ name,
28
+ directory: `${projectKindDirectory(kind)}/${name}`,
29
+ stack: CONFORMANCE_STACK,
30
+ workspace: { name: 'conformance', scope: '@conformance', modulePrefix: 'example.com/conformance' },
31
+ };
32
+ let files;
33
+ try {
34
+ const generated = await client.call('generate', params, { timeoutMs: LONG_MS });
35
+ files = generated.files;
36
+ }
37
+ catch (error) {
38
+ report.add(`generates ${article(kind)}`, false, messageOf(error));
39
+ continue;
40
+ }
41
+ const missing = files.filter(file => !existsSync(join(workspace, ...file.split('/'))));
42
+ report.add(`generates ${article(kind)}`, files.length > 0 && missing.length === 0, `files reported but not written: ${goList(missing)} (of ${goList(files)})`);
43
+ made.push(params);
44
+ }
45
+ if (!language || made.length === 0)
46
+ return;
47
+ let projects;
48
+ try {
49
+ const discovered = await client.call('discover', { manifests: findManifests(workspace, client.info.manifests ?? []) }, { timeoutMs: LONG_MS });
50
+ projects = discovered.projects;
51
+ }
52
+ catch (error) {
53
+ report.add('discovers what it generated', false, messageOf(error));
54
+ return;
55
+ }
56
+ const found = new Map(projects.map(project => [project.root, project]));
57
+ for (const params of made) {
58
+ const project = found.get(params.directory);
59
+ report.add(`discovers the generated ${params.kind}`, project !== undefined, `no project at ${params.directory} among ${projects.length} discovered`);
60
+ if (project === undefined)
61
+ continue;
62
+ report.add(`the generated ${params.kind} keeps its kind`, project.kind === params.kind, `discovered as ${goQuote(project.kind)}`);
63
+ report.add(`the generated ${params.kind} records its stack`, project.stack === CONFORMANCE_STACK, `discovered with stack ${goQuote(project.stack ?? '')}; generate must record params.stack in the manifest`);
64
+ const empty = Object.entries(project.tasks ?? {}).filter(([, task]) => task.command.length === 0).map(([name]) => name).sort((a, b) => a.localeCompare(b));
65
+ report.add(`the generated ${params.kind}'s tasks have commands`, empty.length === 0, `tasks without a command: ${goList(empty)}`);
66
+ if (client.supports('version/write')) {
67
+ await attempt(report, `writes the ${params.kind}'s version (dry run)`, () => client.call('version/write', { project, version: '1.2.3', dryRun: true }, { timeoutMs: 60_000 }));
68
+ }
69
+ if (client.supports('remove')) {
70
+ await attempt(report, `would remove the ${params.kind} (dry run)`, () => client.call('remove', { project, dryRun: true }, { timeoutMs: 60_000 }));
71
+ }
72
+ if (params.kind === 'package' && client.supports('publish')) {
73
+ await attempt(report, 'publishes the package (dry run)', () => client.call('publish', { project, version: '1.2.3', dryRun: true }, { timeoutMs: 5 * 60_000 }));
74
+ }
75
+ if (params.kind === 'package' && client.supports('published')) {
76
+ await attempt(report, 'says what the registry has of the package', () => client.call('published', { project }, { timeoutMs: 60_000 }), ' (an unreachable registry is a result, not an error)');
77
+ }
78
+ }
79
+ }
80
+ async function attempt(report, check, call, hint = '') {
81
+ try {
82
+ await call();
83
+ report.add(check, true, '');
84
+ }
85
+ catch (error) {
86
+ report.add(check, false, `${messageOf(error)}${hint}`);
87
+ }
88
+ }
89
+ function messageOf(thrown) {
90
+ return thrown instanceof Error ? thrown.message : String(thrown);
91
+ }
@@ -0,0 +1,25 @@
1
+ import type { PluginClient } from '../host/index.ts';
2
+ import { ConformanceReport } from './conformance-report.model.ts';
3
+ /** Options of {@link checkPlugin}. */
4
+ export interface CheckOptions {
5
+ /**
6
+ * An empty directory the plugin was started in (its `workspaceRoot`). When
7
+ * set, a plugin that generates projects is asked to generate an app, a lib
8
+ * and a package there, which a language plugin must then discover as
9
+ * generated. Generation may install dependencies, so it is opt-in.
10
+ */
11
+ workspace?: string;
12
+ }
13
+ /**
14
+ * Runs the checks of `smith plugin check` against a connected plugin: it names
15
+ * itself, has a known kind and known roles, its roles match what it
16
+ * implements, it names the slots it fills, its references and stacks are
17
+ * well-formed, every advertised method answers a request with nothing in it,
18
+ * requests sent together are answered as the declared `concurrency` allows, a
19
+ * plugin that advertises `cancel` stops a cancelled request, and, with `workspace`, the projects it generates are found again by its own
20
+ * `discover`. A port of the Go SDK's `conformance.Check`, with the same checks
21
+ * under the same names. (`smith plugin check` adds one more, "shuts down when
22
+ * asked", after closing the plugin: call {@link PluginClient.close} and add it
23
+ * to the report if you want it.)
24
+ */
25
+ export declare function checkPlugin(client: PluginClient, options?: CheckOptions): Promise<ConformanceReport>;
@@ -0,0 +1,81 @@
1
+ import { describeRef, isEmptyRef } from '../plugin-refs/index.js';
2
+ import { ROLES, effectiveRoles, isKnownRole } from '../plugin-roles/index.js';
3
+ import { checkGeneration } from './check-generation.use-case.js';
4
+ import { checkScheduling } from './check-scheduling.use-case.js';
5
+ import { ConformanceReport } from './conformance-report.model.js';
6
+ import { goList, goQuote } from './go-list.mapper.js';
7
+ const CALL_TIMEOUT_MS = 20 * 60 * 1000;
8
+ /**
9
+ * Runs the checks of `smith plugin check` against a connected plugin: it names
10
+ * itself, has a known kind and known roles, its roles match what it
11
+ * implements, it names the slots it fills, its references and stacks are
12
+ * well-formed, every advertised method answers a request with nothing in it,
13
+ * requests sent together are answered as the declared `concurrency` allows, a
14
+ * plugin that advertises `cancel` stops a cancelled request, and, with `workspace`, the projects it generates are found again by its own
15
+ * `discover`. A port of the Go SDK's `conformance.Check`, with the same checks
16
+ * under the same names. (`smith plugin check` adds one more, "shuts down when
17
+ * asked", after closing the plugin: call {@link PluginClient.close} and add it
18
+ * to the report if you want it.)
19
+ */
20
+ export async function checkPlugin(client, options = {}) {
21
+ const info = client.info;
22
+ const report = new ConformanceReport(info.plugin);
23
+ report.add('names itself', info.plugin.name !== '' && info.plugin.version !== '', `initialize returned name ${goQuote(info.plugin.name)}, version ${goQuote(info.plugin.version)}; both are required`);
24
+ report.add('has a known kind', info.kind === 'language' || info.kind === 'convention', `kind ${goQuote(info.kind)} is neither language nor convention`);
25
+ const roles = effectiveRoles(info);
26
+ for (const role of roles)
27
+ report.add(`plays a known role: ${role}`, isKnownRole(role), `roles are ${goList(ROLES)}`);
28
+ const caps = info.capabilities;
29
+ if (roles.includes('language')) {
30
+ report.add('a language plugin discovers projects', caps.discover === true && (info.manifests ?? []).length > 0, 'it must implement discover and name its manifest files');
31
+ }
32
+ if (roles.includes('scaffold'))
33
+ report.add('a scaffold generates projects', caps.generate === true, 'it must implement generate');
34
+ for (const role of ['toolchain', 'structure', 'deploy']) {
35
+ if (roles.includes(role) && !roles.includes('language')) {
36
+ report.add(`a ${role} plugin does something`, caps.contribute === true || caps.generate === true || caps.ci === true || caps.doctor === true, 'it implements none of contribute, generate, ci and doctor');
37
+ }
38
+ }
39
+ const provides = info.provides ?? [];
40
+ if (provides.length > 0)
41
+ report.add('only a toolchain provides tasks', roles.includes('toolchain'), `it provides ${goList(provides)} without the toolchain role`);
42
+ if (info.kind === 'convention' && roles.includes('toolchain'))
43
+ report.add('a toolchain names the slots it fills', provides.length > 0, 'a toolchain plugin must list the tasks it provides');
44
+ const requires = info.requires ?? [];
45
+ const conflicts = info.conflicts ?? [];
46
+ const stacks = info.stacks ?? [];
47
+ for (const ref of requires)
48
+ report.add(`requires ${describeRef(ref)}: a known reference`, isValidRef(ref), 'a reference sets a known level, a name or a slot');
49
+ for (const ref of conflicts) {
50
+ report.add(`conflicts with ${describeRef(ref)}: a known reference`, isValidRef(ref) && ref.name !== info.plugin.name, 'a reference sets a known level, a name or a slot, and is not the plugin itself');
51
+ }
52
+ const names = new Set();
53
+ for (const stack of stacks) {
54
+ report.add(`stack ${stack.name} is complete`, stack.name !== '' && stack.language !== '' && !names.has(stack.name), 'a stack needs a unique name and a language plugin');
55
+ names.add(stack.name);
56
+ }
57
+ await checkScheduling(client, report);
58
+ const answers = async (check, method, params) => {
59
+ if (!client.supports(method))
60
+ return;
61
+ try {
62
+ await client.call(method, params, { timeoutMs: CALL_TIMEOUT_MS });
63
+ report.add(check, true, '');
64
+ }
65
+ catch (error) {
66
+ report.add(check, false, error instanceof Error ? error.message : String(error));
67
+ }
68
+ };
69
+ await answers('discover answers with no manifests', 'discover', { manifests: [] });
70
+ await answers('contribute answers with no projects', 'contribute', { projects: [] });
71
+ await answers('ci answers with no projects', 'ci', { projects: [] });
72
+ await answers('doctor answers with no projects', 'doctor', { projects: [] });
73
+ if (options.workspace !== undefined && caps.generate === true)
74
+ await checkGeneration(client, options.workspace, report);
75
+ return report;
76
+ }
77
+ /** Whether a reference names something: a known level, a name or a slot. */
78
+ function isValidRef(ref) {
79
+ const level = ref.level ?? '';
80
+ return !isEmptyRef(ref) && (level === '' || isKnownRole(level));
81
+ }
@@ -0,0 +1,11 @@
1
+ import type { PluginClient } from '../host/index.ts';
2
+ import type { ConformanceReport } from './conformance-report.model.ts';
3
+ /** How long a plugin that advertised cancel has to answer a request the host cancelled. */
4
+ export declare const CANCEL_BOUND_MS = 10000;
5
+ /**
6
+ * Checks what a plugin says about how it is called: the concurrency it
7
+ * declares must be one the protocol defines and be accepted, and the cancel it
8
+ * advertises must stop a request within a bound and leave the plugin serving.
9
+ * The same checks, under the same names, as the Go SDK's.
10
+ */
11
+ export declare function checkScheduling(client: PluginClient, report: ConformanceReport, cancelBoundMs?: number): Promise<void>;
@@ -0,0 +1,98 @@
1
+ import { CallInterruptedError } from '../host/index.js';
2
+ import { ERROR_CODES } from '../protocol/index.js';
3
+ import { inFlightLimit, isValidConcurrency } from '../concurrency/index.js';
4
+ /** How long a plugin that advertised cancel has to answer a request the host cancelled. */
5
+ export const CANCEL_BOUND_MS = 10_000;
6
+ /** An id no request ever had. */
7
+ const UNKNOWN_REQUEST = 2 ** 40;
8
+ /** The cheapest call the plugin answers without doing work: a method that takes a list of projects or manifests, asked about none. */
9
+ function probeOf(client) {
10
+ const timeoutMs = 2 * 60 * 1000;
11
+ if (client.supports('discover'))
12
+ return signal => client.call('discover', { manifests: [] }, { timeoutMs, signal });
13
+ if (client.supports('contribute'))
14
+ return signal => client.call('contribute', { projects: [] }, { timeoutMs, signal });
15
+ if (client.supports('ci'))
16
+ return signal => client.call('ci', { projects: [] }, { timeoutMs, signal });
17
+ return client.supports('doctor') ? signal => client.call('doctor', { projects: [] }, { timeoutMs, signal }) : undefined;
18
+ }
19
+ function messageOf(thrown) {
20
+ return thrown instanceof Error ? thrown.message : String(thrown);
21
+ }
22
+ /**
23
+ * Checks what a plugin says about how it is called: the concurrency it
24
+ * declares must be one the protocol defines and be accepted, and the cancel it
25
+ * advertises must stop a request within a bound and leave the plugin serving.
26
+ * The same checks, under the same names, as the Go SDK's.
27
+ */
28
+ export async function checkScheduling(client, report, cancelBoundMs = CANCEL_BOUND_MS) {
29
+ const info = client.info;
30
+ report.add('declares a concurrency the protocol defines', isValidConcurrency(info.concurrency ?? 0), `concurrency ${info.concurrency} is below -1; it must be absent, 1 or more, or -1 for unlimited`);
31
+ const probe = probeOf(client);
32
+ if (probe === undefined)
33
+ return;
34
+ await checkConcurrency(client, probe, report);
35
+ if (info.capabilities.cancel === true)
36
+ await checkCancel(client, probe, report, cancelBoundMs);
37
+ }
38
+ /**
39
+ * Sends requests together. A plugin that declares nothing is sent them one at
40
+ * a time and must answer them all; one that declares a concurrency is sent that
41
+ * many at once (at most four, to keep the check cheap) and must answer them all.
42
+ */
43
+ async function checkConcurrency(client, probe, report) {
44
+ const limit = inFlightLimit(client.info.concurrency);
45
+ let together = Math.min(limit, 4);
46
+ let name = `accepts ${together} requests at the same time`;
47
+ if (limit === 1) {
48
+ together = 3;
49
+ name = 'answers 3 requests sent together, one at a time';
50
+ }
51
+ const settled = await Promise.allSettled(Array.from({ length: together }, async () => await probe()));
52
+ const failed = settled.find((result) => result.status === 'rejected');
53
+ const peak = client.peakInFlight;
54
+ report.add(name, failed === undefined && peak <= limit, `${failed === undefined ? '<nil>' : messageOf(failed.reason)} (the host had ${peak} in flight, the plugin declared ${limit})`);
55
+ }
56
+ /** How a promise ended: with the error it rejected with, if any. */
57
+ async function settle(promise) {
58
+ try {
59
+ await promise;
60
+ return {};
61
+ }
62
+ catch (error) {
63
+ return { error };
64
+ }
65
+ }
66
+ /** Cancels a request, then a request that does not exist. */
67
+ async function checkCancel(client, probe, report, cancelBoundMs) {
68
+ const controller = new AbortController();
69
+ const began = Date.now();
70
+ const request = settle(probe(controller.signal));
71
+ await new Promise(resolve => setTimeout(resolve, 5)); // let the request leave; a cancel that wins the race is just as good an answer
72
+ controller.abort();
73
+ let timer;
74
+ const late = new Promise((resolve) => {
75
+ timer = setTimeout(() => { resolve('late'); }, cancelBoundMs + client.cancelGraceMs);
76
+ });
77
+ const outcome = await Promise.race([request, late]);
78
+ clearTimeout(timer);
79
+ const error = outcome === 'late' ? new Error(`no answer within ${cancelBoundMs}ms of the cancel`) : outcome.error;
80
+ const stopped = outcome !== 'late' && (error === undefined || (error instanceof CallInterruptedError && !error.killed));
81
+ const detail = `${error === undefined ? '<nil>' : messageOf(error)} (a plugin that advertised cancel answers the request, with ${ERROR_CODES.RequestCancelled} if it stopped, within ${cancelBoundMs}ms)`;
82
+ report.add('answers a cancelled request with its result or the cancelled error', stopped && Date.now() - began < cancelBoundMs, detail);
83
+ let notifyError;
84
+ let followUp;
85
+ try {
86
+ await client.notify('cancel', { id: UNKNOWN_REQUEST });
87
+ }
88
+ catch (error_) {
89
+ notifyError = error_;
90
+ }
91
+ try {
92
+ await probe();
93
+ }
94
+ catch (error_) {
95
+ followUp = error_;
96
+ }
97
+ report.add('ignores a cancel for a request it does not know', notifyError === undefined && followUp === undefined, `${notifyError === undefined ? '<nil>' : messageOf(notifyError)}, then ${followUp === undefined ? '<nil>' : messageOf(followUp)}`);
98
+ }
@@ -0,0 +1,22 @@
1
+ import type { Peer } from '../protocol/index.ts';
2
+ /** One check: its name, whether it held, and, when it did not, what is wrong. */
3
+ export interface CheckResult {
4
+ check: string;
5
+ ok: boolean;
6
+ detail?: string;
7
+ }
8
+ /**
9
+ * Every check run against one plugin. `JSON.stringify(report)` gives the shape
10
+ * `smith plugin check --json` prints: `{ plugin, results }`.
11
+ */
12
+ export declare class ConformanceReport {
13
+ readonly plugin: Peer;
14
+ readonly results: CheckResult[];
15
+ constructor(plugin: Peer);
16
+ /** Whether every check passed. */
17
+ get ok(): boolean;
18
+ /** The failed checks, one per line: `check: detail`. */
19
+ failures(): string;
20
+ /** Records a check. The detail is kept only for a check that failed. */
21
+ add(check: string, ok: boolean, detail: string): void;
22
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Every check run against one plugin. `JSON.stringify(report)` gives the shape
3
+ * `smith plugin check --json` prints: `{ plugin, results }`.
4
+ */
5
+ export class ConformanceReport {
6
+ plugin;
7
+ results = [];
8
+ constructor(plugin) {
9
+ this.plugin = plugin;
10
+ }
11
+ /** Whether every check passed. */
12
+ get ok() {
13
+ return this.results.every(result => result.ok);
14
+ }
15
+ /** The failed checks, one per line: `check: detail`. */
16
+ failures() {
17
+ return this.results.filter(result => !result.ok).map(result => `${result.check}: ${result.detail ?? ''}`).join('\n');
18
+ }
19
+ /** Records a check. The detail is kept only for a check that failed. */
20
+ add(check, ok, detail) {
21
+ this.results.push(ok ? { check, ok } : { check, ok, detail });
22
+ }
23
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * The manifests of a plugin below a workspace, workspace-relative with forward
3
+ * slashes: files named like `names` (see isManifest), skipping dot-folders and
4
+ * dependency folders, and files at the root itself, as the Go conformance
5
+ * suite does.
6
+ */
7
+ export declare function findManifests(workspace: string, names: readonly string[]): string[];
@@ -0,0 +1,34 @@
1
+ import { readdirSync } from 'node:fs';
2
+ import { join, posix } from 'node:path';
3
+ import { isManifest } from '../projects/index.js';
4
+ const SKIPPED_FOLDERS = new Set(['node_modules', '__pycache__']);
5
+ /**
6
+ * The manifests of a plugin below a workspace, workspace-relative with forward
7
+ * slashes: files named like `names` (see isManifest), skipping dot-folders and
8
+ * dependency folders, and files at the root itself, as the Go conformance
9
+ * suite does.
10
+ */
11
+ export function findManifests(workspace, names) {
12
+ const found = [];
13
+ const walk = (relative) => {
14
+ const folder = join(workspace, ...relative.split('/').filter(part => part !== ''));
15
+ const entries = readdirSync(folder, { withFileTypes: true });
16
+ for (const entry of entries) {
17
+ const path = relative === '' ? entry.name : `${relative}/${entry.name}`;
18
+ if (entry.isDirectory()) {
19
+ if (!entry.name.startsWith('.') && !SKIPPED_FOLDERS.has(entry.name))
20
+ walk(path);
21
+ }
22
+ else if (isManifest(names, entry.name) && posix.dirname(path) !== '.') {
23
+ found.push(path);
24
+ }
25
+ }
26
+ };
27
+ try {
28
+ walk('');
29
+ }
30
+ catch {
31
+ // An unreadable folder ends the walk; what was found stays.
32
+ }
33
+ return found;
34
+ }
@@ -0,0 +1,4 @@
1
+ /** A list as Go's `%v` writes it, `[a b]`, which the details of the Go checks use; the TypeScript checks keep their text the same. */
2
+ export declare function goList(items: readonly unknown[]): string;
3
+ /** A string as Go's `%q` writes it. */
4
+ export declare function goQuote(text: string): string;
@@ -0,0 +1,8 @@
1
+ /** A list as Go's `%v` writes it, `[a b]`, which the details of the Go checks use; the TypeScript checks keep their text the same. */
2
+ export function goList(items) {
3
+ return `[${items.map(String).join(' ')}]`;
4
+ }
5
+ /** A string as Go's `%q` writes it. */
6
+ export function goQuote(text) {
7
+ return JSON.stringify(text);
8
+ }
@@ -0,0 +1,5 @@
1
+ export { checkPlugin } from './check-plugin.use-case.ts';
2
+ export type { CheckOptions } from './check-plugin.use-case.ts';
3
+ export { CONFORMANCE_STACK } from './check-generation.use-case.ts';
4
+ export { ConformanceReport } from './conformance-report.model.ts';
5
+ export type { CheckResult } from './conformance-report.model.ts';
@@ -0,0 +1,3 @@
1
+ export { checkPlugin } from './check-plugin.use-case.js';
2
+ export { CONFORMANCE_STACK } from './check-generation.use-case.js';
3
+ export { ConformanceReport } from './conformance-report.model.js';