@pulse-compute/cli 0.0.0 → 1.0.0-beta.2
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/API.md +572 -0
- package/CHANGELOG.md +71 -0
- package/README.md +71 -1
- package/bin/pulse.js +15 -0
- package/bin/pulsewasm-extract.js +22 -0
- package/cli-spec.json +575 -0
- package/completions/_pulse +106 -0
- package/completions/pulse.bash +31 -0
- package/completions/pulse.fish +76 -0
- package/docs/README.md +77 -0
- package/docs/architecture/current-contracts.md +470 -0
- package/docs/architecture/overview.md +93 -0
- package/docs/architecture/vision.md +515 -0
- package/docs/concepts/bodies.md +186 -0
- package/docs/concepts/compilation-and-lowering.md +496 -0
- package/docs/concepts/contracts-and-providers.md +158 -0
- package/docs/concepts/effects-and-continuations.md +300 -0
- package/docs/concepts/entities-and-adapters.md +71 -0
- package/docs/concepts/package-owned-lowering.md +175 -0
- package/docs/concepts/targets-and-hosts.md +70 -0
- package/docs/contributing/README.md +38 -0
- package/docs/contributing/adding-core-provider.md +315 -0
- package/docs/contributing/adding-first-party-lowerer.md +327 -0
- package/docs/contributing/entities-lowering.md +91 -0
- package/docs/contributing/package-lowerer-contract.md +383 -0
- package/docs/contributing/pulse-aware-packages.md +149 -0
- package/docs/examples.md +112 -0
- package/docs/fixtures/inspect-fetch-composition.selected.json +181 -0
- package/docs/getting-started.md +128 -0
- package/docs/guides/compatibility-imports.md +60 -0
- package/docs/guides/deploying-fastly.md +188 -0
- package/docs/guides/deploying-node.md +143 -0
- package/docs/guides/events.md +233 -0
- package/docs/guides/fastly-capabilities.md +153 -0
- package/docs/guides/fetching-and-composition.md +101 -0
- package/docs/guides/grip.md +94 -0
- package/docs/guides/json-schemas.md +233 -0
- package/docs/guides/migrating-from-express.md +192 -0
- package/docs/guides/project-lifecycle.md +143 -0
- package/docs/guides/routing.md +202 -0
- package/docs/guides/troubleshooting.md +47 -0
- package/docs/maintainers/README.md +60 -0
- package/docs/maintainers/codex-maintainer.md +71 -0
- package/docs/maintainers/documentation-deployment.md +147 -0
- package/docs/maintainers/documentation-system.md +58 -0
- package/docs/maintainers/documentation-versioning.md +65 -0
- package/docs/maintainers/maintainer-charter.md +100 -0
- package/docs/maintainers/maintenance-policy.json +809 -0
- package/docs/maintainers/maintenance-policy.md +124 -0
- package/docs/maintainers/npm-publishing.md +154 -0
- package/docs/maintainers/plugin-readiness.json +19 -0
- package/docs/maintainers/plugin-readiness.md +31 -0
- package/docs/maintainers/public-site.md +158 -0
- package/docs/maintainers/release-acceptance.md +293 -0
- package/docs/maintainers/release-manifest.md +89 -0
- package/docs/maintainers/repository-setup.md +145 -0
- package/docs/maintainers/scope-policy.md +105 -0
- package/docs/maintainers/support-and-triage.md +74 -0
- package/docs/maintainers/testing.md +334 -0
- package/docs/packages/README.md +60 -0
- package/docs/packages/assets.md +85 -0
- package/docs/packages/cli.md +190 -0
- package/docs/packages/crypto.md +78 -0
- package/docs/packages/entities.md +129 -0
- package/docs/packages/grip.md +82 -0
- package/docs/packages/implementation-packages.md +122 -0
- package/docs/packages/jwt.md +55 -0
- package/docs/packages/provider-fastly.md +243 -0
- package/docs/packages/pulse.md +159 -0
- package/docs/packages/runtime.md +265 -0
- package/docs/packages/s3.md +101 -0
- package/docs/preview-scope.md +142 -0
- package/docs/reference/README.md +33 -0
- package/docs/reference/cli-spec.json +575 -0
- package/docs/reference/cli.md +457 -0
- package/docs/reference/compatibility-matrix.md +169 -0
- package/docs/reference/diagnostics.md +1872 -0
- package/docs/reference/documentation-versions.json +22 -0
- package/docs/reference/environment.md +347 -0
- package/docs/reference/handler-authoring.md +264 -0
- package/docs/reference/project-config.md +582 -0
- package/docs/reference/project-config.schema.json +981 -0
- package/docs/reference/release-manifest.json +441 -0
- package/docs/reference/shell-completion.md +23 -0
- package/documentation-site.json +668 -0
- package/documentation-versions.json +22 -0
- package/examples/01-hello-json/.pulse/.gitignore +3 -0
- package/examples/01-hello-json/.pulse/config.ts +16 -0
- package/examples/01-hello-json/README.md +119 -0
- package/examples/01-hello-json/package.json +20 -0
- package/examples/01-hello-json/src/index.ts +9 -0
- package/examples/01-hello-json/tests/pulse.harness.ts +22 -0
- package/examples/01-hello-json/tsconfig.json +14 -0
- package/examples/02-request-schema/.pulse/.gitignore +3 -0
- package/examples/02-request-schema/.pulse/config.ts +17 -0
- package/examples/02-request-schema/README.md +145 -0
- package/examples/02-request-schema/package.json +20 -0
- package/examples/02-request-schema/src/index.ts +18 -0
- package/examples/02-request-schema/src/schemas.ts +20 -0
- package/examples/02-request-schema/tests/pulse.harness.ts +37 -0
- package/examples/02-request-schema/tsconfig.json +14 -0
- package/examples/03-fetch-composition/.pulse/.gitignore +3 -0
- package/examples/03-fetch-composition/.pulse/config.ts +31 -0
- package/examples/03-fetch-composition/README.md +210 -0
- package/examples/03-fetch-composition/package.json +20 -0
- package/examples/03-fetch-composition/src/index.ts +60 -0
- package/examples/03-fetch-composition/tests/pulse.harness.ts +47 -0
- package/examples/03-fetch-composition/tsconfig.json +14 -0
- package/examples/05-fastly-capabilities/.pulse/.gitignore +3 -0
- package/examples/05-fastly-capabilities/.pulse/config.ts +57 -0
- package/examples/05-fastly-capabilities/README.md +257 -0
- package/examples/05-fastly-capabilities/package.json +22 -0
- package/examples/05-fastly-capabilities/src/index.ts +55 -0
- package/examples/05-fastly-capabilities/tests/pulse.harness.ts +66 -0
- package/examples/05-fastly-capabilities/tsconfig.json +14 -0
- package/examples/07-opaque-proxy/.pulse/.gitignore +3 -0
- package/examples/07-opaque-proxy/.pulse/config.ts +38 -0
- package/examples/07-opaque-proxy/README.md +150 -0
- package/examples/07-opaque-proxy/package.json +21 -0
- package/examples/07-opaque-proxy/src/index.ts +9 -0
- package/examples/07-opaque-proxy/tests/pulse.harness.ts +25 -0
- package/examples/07-opaque-proxy/tsconfig.json +14 -0
- package/examples/09-router-lowering/.pulse/.gitignore +3 -0
- package/examples/09-router-lowering/.pulse/config.ts +22 -0
- package/examples/09-router-lowering/README.md +287 -0
- package/examples/09-router-lowering/package.json +22 -0
- package/examples/09-router-lowering/src/index.ts +65 -0
- package/examples/09-router-lowering/tests/pulse.harness.ts +91 -0
- package/examples/09-router-lowering/tsconfig.json +14 -0
- package/examples/10-entities-tools/.pulse/.gitignore +4 -0
- package/examples/10-entities-tools/.pulse/config.ts +23 -0
- package/examples/10-entities-tools/README.md +165 -0
- package/examples/10-entities-tools/package.json +21 -0
- package/examples/10-entities-tools/src/handlers.ts +15 -0
- package/examples/10-entities-tools/src/index.ts +31 -0
- package/examples/10-entities-tools/src/schemas.ts +18 -0
- package/examples/10-entities-tools/tests/pulse.harness.ts +40 -0
- package/examples/10-entities-tools/tools-facade.cjs +158 -0
- package/examples/10-entities-tools/tsconfig.json +14 -0
- package/examples/11-events/.pulse/.gitignore +4 -0
- package/examples/11-events/.pulse/config.ts +24 -0
- package/examples/11-events/README.md +194 -0
- package/examples/11-events/package.json +20 -0
- package/examples/11-events/src/index.ts +27 -0
- package/examples/11-events/src/schemas.ts +19 -0
- package/examples/11-events/tests/pulse.harness.ts +34 -0
- package/examples/11-events/tsconfig.json +15 -0
- package/examples/12-mcp-proxy/.pulse/.gitignore +3 -0
- package/examples/12-mcp-proxy/.pulse/config.ts +25 -0
- package/examples/12-mcp-proxy/README.md +149 -0
- package/examples/12-mcp-proxy/package.json +20 -0
- package/examples/12-mcp-proxy/src/index.ts +17 -0
- package/examples/12-mcp-proxy/tests/pulse.harness.ts +29 -0
- package/examples/12-mcp-proxy/tsconfig.json +14 -0
- package/examples/13-jwt-es256/.pulse/config.ts +19 -0
- package/examples/13-jwt-es256/README.md +195 -0
- package/examples/13-jwt-es256/package.json +21 -0
- package/examples/13-jwt-es256/src/index.ts +21 -0
- package/examples/13-jwt-es256/tests/pulse.harness.ts +54 -0
- package/examples/13-jwt-es256/tsconfig.json +14 -0
- package/examples/README.md +36 -0
- package/package.json +66 -6
- package/project-config.schema.json +981 -0
- package/release-manifest.json +441 -0
- package/src/command-spec.js +279 -0
- package/src/completion.js +113 -0
- package/src/diagnostics.js +350 -0
- package/src/documentation.js +45 -0
- package/src/index.d.ts +133 -0
- package/src/index.js +15 -0
- package/src/internal/command-executor.d.ts +29 -0
- package/src/internal/command-executor.js +143 -0
- package/src/internal/command-plan.d.ts +9 -0
- package/src/internal/command-plan.js +73 -0
- package/src/internal/command-reporter.d.ts +15 -0
- package/src/internal/command-reporter.js +133 -0
- package/src/internal/command-request.d.ts +35 -0
- package/src/internal/command-request.js +154 -0
- package/src/internal/node-http.js +54 -0
- package/src/internal/project-context.d.ts +66 -0
- package/src/internal/project-context.js +175 -0
- package/src/project-config-reference.js +4 -0
- package/src/project-config-schema.d.ts +23 -0
- package/src/project-config-schema.js +271 -0
- package/src/project-config.js +724 -0
- package/src/project-execution.js +2946 -0
- package/src/provider-drivers.js +3 -0
- package/src/target-support.js +3 -0
- package/src/typescript-module-loader.js +269 -0
- package/src/workflow.js +78 -0
- package/src/workspace.js +82 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
type CustomerLookupInput = Readonly<{
|
|
2
|
+
email: string
|
|
3
|
+
}>
|
|
4
|
+
|
|
5
|
+
export async function systemStatus(_ctx: unknown, _input: undefined) {
|
|
6
|
+
return undefined
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function lookupCustomer(ctx: any, input: CustomerLookupInput) {
|
|
10
|
+
const displayName = await ctx.fetch(
|
|
11
|
+
`https://directory.example.test/customers/${input.email}`,
|
|
12
|
+
).text()
|
|
13
|
+
return { email: input.email, displayName }
|
|
14
|
+
}
|
|
15
|
+
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { EntityRouter, jsonRpc } from '@pulse-compute/entities'
|
|
2
|
+
import { lookupCustomer, systemStatus } from './handlers.js'
|
|
3
|
+
|
|
4
|
+
const rpc = new EntityRouter({
|
|
5
|
+
adapter: jsonRpc({ namedParamsOnly: true, acceptEmptyObjectForNoInput: true }),
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
rpc.on('system.status', {
|
|
9
|
+
input: null,
|
|
10
|
+
output: null,
|
|
11
|
+
metadata: {
|
|
12
|
+
title: 'Check system status',
|
|
13
|
+
description: 'Checks that the governed entity request boundary is available.',
|
|
14
|
+
mcp: { readOnlyHint: true },
|
|
15
|
+
},
|
|
16
|
+
}, systemStatus)
|
|
17
|
+
|
|
18
|
+
rpc.on('customer.lookup', {
|
|
19
|
+
input: 'tools.CustomerLookupInput',
|
|
20
|
+
output: 'tools.CustomerLookupOutput',
|
|
21
|
+
metadata: {
|
|
22
|
+
title: 'Look up customer',
|
|
23
|
+
description: 'Looks up one customer through the configured directory backend.',
|
|
24
|
+
mcp: { readOnlyHint: true },
|
|
25
|
+
},
|
|
26
|
+
}, lookupCustomer)
|
|
27
|
+
|
|
28
|
+
export default async function handler(ctx: unknown) {
|
|
29
|
+
return rpc.handle(ctx as never)
|
|
30
|
+
}
|
|
31
|
+
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { defineSchemaRegistry, schema } from '@pulse-compute/pulse/schema'
|
|
2
|
+
|
|
3
|
+
export interface CustomerLookupInput {
|
|
4
|
+
email: string
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface CustomerLookupOutput {
|
|
8
|
+
email: string
|
|
9
|
+
displayName: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default defineSchemaRegistry({
|
|
13
|
+
schemas: {
|
|
14
|
+
'tools.CustomerLookupInput': schema<CustomerLookupInput>(),
|
|
15
|
+
'tools.CustomerLookupOutput': schema<CustomerLookupOutput>(),
|
|
16
|
+
},
|
|
17
|
+
})
|
|
18
|
+
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export default { cases: [
|
|
2
|
+
{
|
|
3
|
+
name: 'system-status',
|
|
4
|
+
request: {
|
|
5
|
+
method: 'POST',
|
|
6
|
+
path: '/',
|
|
7
|
+
headers: { 'content-type': 'application/json' },
|
|
8
|
+
body: JSON.stringify({ jsonrpc: '2.0', method: 'system.status', id: 'status-doc' }),
|
|
9
|
+
},
|
|
10
|
+
expect: {
|
|
11
|
+
status: 200,
|
|
12
|
+
json: { jsonrpc: '2.0', result: null, id: 'status-doc' },
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
name: 'customer-lookup',
|
|
17
|
+
request: {
|
|
18
|
+
method: 'POST',
|
|
19
|
+
path: '/',
|
|
20
|
+
headers: { 'content-type': 'application/json' },
|
|
21
|
+
body: JSON.stringify({
|
|
22
|
+
jsonrpc: '2.0',
|
|
23
|
+
method: 'customer.lookup',
|
|
24
|
+
params: { email: 'ada@example.test' },
|
|
25
|
+
id: 'lookup-doc',
|
|
26
|
+
}),
|
|
27
|
+
},
|
|
28
|
+
fetches: {
|
|
29
|
+
'https://directory.example.test/customers/ada@example.test': { value: 'Ada Lovelace' },
|
|
30
|
+
},
|
|
31
|
+
expect: {
|
|
32
|
+
status: 200,
|
|
33
|
+
json: {
|
|
34
|
+
jsonrpc: '2.0',
|
|
35
|
+
result: { email: 'ada@example.test', displayName: 'Ada Lovelace' },
|
|
36
|
+
id: 'lookup-doc',
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
] }
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const TOOLS_FACADE_VERSION = 'pulse.entities-tools-facade-demo.v1';
|
|
4
|
+
const TOOLS_CATALOG_VERSION = 'pulse.entities-tools-catalog-demo.v1';
|
|
5
|
+
|
|
6
|
+
const TOOLS_FACADE_POLICY = Object.freeze({
|
|
7
|
+
completeMcpServer: false,
|
|
8
|
+
discovery: 'static-entity-catalog',
|
|
9
|
+
invocation: 'json-rpc-2.0-request-boundary',
|
|
10
|
+
governedExecution: 'pulse-entity-handler',
|
|
11
|
+
excluded: Object.freeze([
|
|
12
|
+
'sse',
|
|
13
|
+
'sessions',
|
|
14
|
+
'tasks',
|
|
15
|
+
'resources',
|
|
16
|
+
'prompts',
|
|
17
|
+
'sampling',
|
|
18
|
+
'authorization',
|
|
19
|
+
'transport-negotiation',
|
|
20
|
+
'runtime-core-protocol-state'
|
|
21
|
+
])
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
class ToolsFacadeInvocationError extends Error {
|
|
25
|
+
constructor(code, message) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.name = 'ToolsFacadeInvocationError';
|
|
28
|
+
this.code = code;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function staticMetadata(value) {
|
|
33
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : Object.freeze({});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function projectTool(entity) {
|
|
37
|
+
const metadata = staticMetadata(entity.metadata);
|
|
38
|
+
const hints = staticMetadata(metadata.mcp);
|
|
39
|
+
return Object.freeze({
|
|
40
|
+
name: entity.name,
|
|
41
|
+
title: typeof metadata.title === 'string' ? metadata.title : entity.name,
|
|
42
|
+
description: typeof metadata.description === 'string' ? metadata.description : '',
|
|
43
|
+
inputSchemaId: entity.inputSchema,
|
|
44
|
+
outputSchemaId: entity.outputSchema,
|
|
45
|
+
annotations: Object.freeze({ readOnlyHint: hints.readOnlyHint === true })
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function readTools(catalog) {
|
|
50
|
+
if (!catalog || catalog.version !== 'pulse.entities-catalog.v1') {
|
|
51
|
+
throw new TypeError('Tools facade requires a pulse.entities-catalog.v1 artifact.');
|
|
52
|
+
}
|
|
53
|
+
if (!Array.isArray(catalog.routers) || catalog.routers.length !== 1) {
|
|
54
|
+
throw new TypeError('Tools facade demonstration requires exactly one static entity router.');
|
|
55
|
+
}
|
|
56
|
+
const router = catalog.routers[0];
|
|
57
|
+
if (!router || router.adapter !== 'json-rpc' || !Array.isArray(router.entities)) {
|
|
58
|
+
throw new TypeError('Tools facade demonstration requires one JSON-RPC entity router.');
|
|
59
|
+
}
|
|
60
|
+
const tools = router.entities.map(projectTool);
|
|
61
|
+
if (new Set(tools.map((tool) => tool.name)).size !== tools.length) {
|
|
62
|
+
throw new TypeError('Tools facade catalog contains duplicate tool names.');
|
|
63
|
+
}
|
|
64
|
+
return Object.freeze(tools);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function requestId(value) {
|
|
68
|
+
if (value === null || typeof value === 'string' || Number.isFinite(value)) return value;
|
|
69
|
+
throw new TypeError('Tools facade request ID must be a string, finite number, or null.');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function createToolsFacade({ catalog, invoke }) {
|
|
73
|
+
if (typeof invoke !== 'function') {
|
|
74
|
+
throw new TypeError(
|
|
75
|
+
'Tools facade requires an injected request-boundary invoker.'
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
const tools = readTools(catalog);
|
|
79
|
+
const toolsByName = new Map(tools.map((tool) => [tool.name, tool]));
|
|
80
|
+
|
|
81
|
+
return Object.freeze({
|
|
82
|
+
version: TOOLS_FACADE_VERSION,
|
|
83
|
+
policy: TOOLS_FACADE_POLICY,
|
|
84
|
+
listTools() {
|
|
85
|
+
return Object.freeze({ version: TOOLS_CATALOG_VERSION, tools });
|
|
86
|
+
},
|
|
87
|
+
async callTool(name, input, options = {}) {
|
|
88
|
+
const tool = toolsByName.get(name);
|
|
89
|
+
if (!tool) throw new RangeError(`Unknown catalog tool ${String(name)}.`);
|
|
90
|
+
if (
|
|
91
|
+
tool.inputSchemaId !== null
|
|
92
|
+
&& (input === null || typeof input !== 'object' || Array.isArray(input))
|
|
93
|
+
) {
|
|
94
|
+
throw new TypeError(`Tool ${name} requires a named object input.`);
|
|
95
|
+
}
|
|
96
|
+
if (tool.inputSchemaId === null && input !== undefined) {
|
|
97
|
+
throw new TypeError(`Tool ${name} does not accept input.`);
|
|
98
|
+
}
|
|
99
|
+
const envelope = {
|
|
100
|
+
jsonrpc: '2.0',
|
|
101
|
+
method: name,
|
|
102
|
+
...(tool.inputSchemaId === null ? {} : { params: input }),
|
|
103
|
+
id: requestId(options.id === undefined ? `tools:${name}` : options.id)
|
|
104
|
+
};
|
|
105
|
+
const response = await invoke(Object.freeze({
|
|
106
|
+
method: 'POST',
|
|
107
|
+
headers: Object.freeze([Object.freeze(['content-type', 'application/json'])]),
|
|
108
|
+
body: JSON.stringify(envelope)
|
|
109
|
+
}));
|
|
110
|
+
const status = Number(response && response.status);
|
|
111
|
+
const body = response && typeof response.body === 'string' ? response.body : '';
|
|
112
|
+
if (status !== 200 || body.length === 0) {
|
|
113
|
+
throw new ToolsFacadeInvocationError(
|
|
114
|
+
'PULSE_TOOLS_BOUNDARY_RESPONSE_INVALID',
|
|
115
|
+
'Tool request boundary returned an invalid response.'
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
let payload;
|
|
119
|
+
try { payload = JSON.parse(body); }
|
|
120
|
+
catch (_) {
|
|
121
|
+
throw new ToolsFacadeInvocationError(
|
|
122
|
+
'PULSE_TOOLS_BOUNDARY_JSON_INVALID',
|
|
123
|
+
'Tool request boundary returned invalid JSON.'
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
if (
|
|
127
|
+
!payload
|
|
128
|
+
|| payload.jsonrpc !== '2.0'
|
|
129
|
+
|| JSON.stringify(payload.id) !== JSON.stringify(envelope.id)
|
|
130
|
+
) {
|
|
131
|
+
throw new ToolsFacadeInvocationError(
|
|
132
|
+
'PULSE_TOOLS_BOUNDARY_ENVELOPE_INVALID',
|
|
133
|
+
'Tool request boundary returned an invalid JSON-RPC envelope.'
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
if (payload.error) {
|
|
137
|
+
throw new ToolsFacadeInvocationError(
|
|
138
|
+
'PULSE_TOOLS_CALL_FAILED',
|
|
139
|
+
typeof payload.error.message === 'string' ? payload.error.message : 'Tool call failed.'
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
if (!Object.prototype.hasOwnProperty.call(payload, 'result')) {
|
|
143
|
+
throw new ToolsFacadeInvocationError(
|
|
144
|
+
'PULSE_TOOLS_BOUNDARY_RESULT_MISSING',
|
|
145
|
+
'Tool request boundary omitted the result.'
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
return payload.result;
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
module.exports = Object.freeze({
|
|
154
|
+
TOOLS_FACADE_POLICY,
|
|
155
|
+
TOOLS_FACADE_VERSION,
|
|
156
|
+
ToolsFacadeInvocationError,
|
|
157
|
+
createToolsFacade
|
|
158
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { defineConfig } from '@pulse-compute/pulse'
|
|
2
|
+
|
|
3
|
+
export default defineConfig((_scope) => ({
|
|
4
|
+
pulse: {
|
|
5
|
+
entry: 'src/index.ts',
|
|
6
|
+
schema: 'src/schemas.ts',
|
|
7
|
+
tests: 'tests/pulse.harness.ts',
|
|
8
|
+
defaultProfile: 'node-native',
|
|
9
|
+
strict: true,
|
|
10
|
+
},
|
|
11
|
+
'node-native': {
|
|
12
|
+
host: 'node',
|
|
13
|
+
target: 'native',
|
|
14
|
+
outDir: 'dist-node-native',
|
|
15
|
+
dev: { host: '127.0.0.1', port: 8787, networkFetch: false },
|
|
16
|
+
},
|
|
17
|
+
'node-javascript': {
|
|
18
|
+
host: 'node',
|
|
19
|
+
target: 'javascript',
|
|
20
|
+
outDir: 'dist-node-javascript',
|
|
21
|
+
dev: { host: '127.0.0.1', port: 8787, networkFetch: false },
|
|
22
|
+
},
|
|
23
|
+
}))
|
|
24
|
+
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# Static events
|
|
2
|
+
|
|
3
|
+
This mixed Node project keeps HTTP and event entries in separate planes. Two
|
|
4
|
+
schema-bound `Pulse.on` registrations accept canonical event frames and use the
|
|
5
|
+
one-way `ctx.emit` effect. The harness proves exact outbound acceptance; it
|
|
6
|
+
does not claim delivery or invoke a matching local handler.
|
|
7
|
+
|
|
8
|
+
## Workflow
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pulse doctor
|
|
12
|
+
pulse inspect
|
|
13
|
+
pulse test
|
|
14
|
+
pulse dev
|
|
15
|
+
pulse build
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`pulse test` executes both HTTP and event cases on the selected Node target.
|
|
19
|
+
`pulse dev` serves only the HTTP portion; Pulse does not expose a public event
|
|
20
|
+
listener or injection command.
|
|
21
|
+
|
|
22
|
+
The documented result below is executed by the documentation gate.
|
|
23
|
+
|
|
24
|
+
<!-- pulse-doc-run {"project":"examples/11-events","args":["test","--json"],"timeoutMs":240000} -->
|
|
25
|
+
```bash
|
|
26
|
+
pulse test --json
|
|
27
|
+
```
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"status": "passed",
|
|
31
|
+
"provider": "node",
|
|
32
|
+
"summary": {
|
|
33
|
+
"total": 3,
|
|
34
|
+
"passed": 3,
|
|
35
|
+
"failed": 0
|
|
36
|
+
},
|
|
37
|
+
"cases": [
|
|
38
|
+
{
|
|
39
|
+
"name": "http health remains separate",
|
|
40
|
+
"status": "passed",
|
|
41
|
+
"response": {
|
|
42
|
+
"status": 200
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"name": "schema event emits an accepted frame",
|
|
47
|
+
"status": "passed",
|
|
48
|
+
"kind": "event",
|
|
49
|
+
"emittedFrameCount": 1
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Wasm size
|
|
56
|
+
|
|
57
|
+
| Artifact | Default build | `--experimental-native-size` | Reduction |
|
|
58
|
+
|---|---:|---:|---:|
|
|
59
|
+
| `canonical-native.wasm` | 38.4 KiB (39,358 bytes) | 30.6 KiB (31,313 bytes) | 20.4% |
|
|
60
|
+
|
|
61
|
+
The executable documentation gate rebuilds and measures both variants on
|
|
62
|
+
`1.0.0-beta.2`. These are uncompressed on-disk sizes, not transfer sizes or
|
|
63
|
+
platform limits.
|
|
64
|
+
|
|
65
|
+
## Application
|
|
66
|
+
|
|
67
|
+
<!-- pulse-doc-source: examples/11-events/src/index.ts -->
|
|
68
|
+
```ts
|
|
69
|
+
import { Pulse } from '@pulse-compute/pulse'
|
|
70
|
+
|
|
71
|
+
type DeviceReading = Readonly<{
|
|
72
|
+
deviceId: string
|
|
73
|
+
temperatureC: number
|
|
74
|
+
}>
|
|
75
|
+
|
|
76
|
+
const app = new Pulse({ auto: true })
|
|
77
|
+
|
|
78
|
+
app.get('/health', async (ctx) => ctx.text('ok'))
|
|
79
|
+
|
|
80
|
+
app.on<DeviceReading>('device.reading', { schema: 'events.DeviceReading' }, async (ctx) => {
|
|
81
|
+
const reading = ctx.event.payload
|
|
82
|
+
ctx.log.info('device reading accepted')
|
|
83
|
+
await ctx.emit('device.reading.accepted', {
|
|
84
|
+
schema: 'events.DeviceReadingAccepted',
|
|
85
|
+
payload: { deviceId: reading.deviceId, accepted: true },
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
app.on('system.tick', { schema: null }, async (ctx) => {
|
|
90
|
+
ctx.log.info('system tick accepted')
|
|
91
|
+
await ctx.emit('system.heartbeat', { schema: null })
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
export default app
|
|
95
|
+
```
|
|
96
|
+
<!-- /pulse-doc-source -->
|
|
97
|
+
|
|
98
|
+
## Schemas
|
|
99
|
+
|
|
100
|
+
<!-- pulse-doc-source: examples/11-events/src/schemas.ts -->
|
|
101
|
+
```ts
|
|
102
|
+
import { defineSchemaRegistry, schema } from '@pulse-compute/pulse/schema'
|
|
103
|
+
|
|
104
|
+
export interface DeviceReading {
|
|
105
|
+
deviceId: string
|
|
106
|
+
temperatureC: number
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface DeviceReadingAccepted {
|
|
110
|
+
deviceId: string
|
|
111
|
+
accepted: boolean
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export default defineSchemaRegistry({
|
|
115
|
+
schemas: {
|
|
116
|
+
'events.DeviceReading': schema<DeviceReading>(),
|
|
117
|
+
'events.DeviceReadingAccepted': schema<DeviceReadingAccepted>(),
|
|
118
|
+
},
|
|
119
|
+
})
|
|
120
|
+
```
|
|
121
|
+
<!-- /pulse-doc-source -->
|
|
122
|
+
|
|
123
|
+
## Project config
|
|
124
|
+
|
|
125
|
+
<!-- pulse-doc-source: examples/11-events/.pulse/config.ts -->
|
|
126
|
+
```ts
|
|
127
|
+
import { defineConfig } from '@pulse-compute/pulse'
|
|
128
|
+
|
|
129
|
+
export default defineConfig((_scope) => ({
|
|
130
|
+
pulse: {
|
|
131
|
+
entry: 'src/index.ts',
|
|
132
|
+
schema: 'src/schemas.ts',
|
|
133
|
+
tests: 'tests/pulse.harness.ts',
|
|
134
|
+
defaultProfile: 'node-native',
|
|
135
|
+
strict: true,
|
|
136
|
+
},
|
|
137
|
+
'node-native': {
|
|
138
|
+
host: 'node',
|
|
139
|
+
target: 'native',
|
|
140
|
+
outDir: 'dist-node-native',
|
|
141
|
+
dev: { host: '127.0.0.1', port: 8787, networkFetch: false },
|
|
142
|
+
},
|
|
143
|
+
'node-javascript': {
|
|
144
|
+
host: 'node',
|
|
145
|
+
target: 'javascript',
|
|
146
|
+
outDir: 'dist-node-javascript',
|
|
147
|
+
dev: { host: '127.0.0.1', port: 8787, networkFetch: false },
|
|
148
|
+
},
|
|
149
|
+
}))
|
|
150
|
+
```
|
|
151
|
+
<!-- /pulse-doc-source -->
|
|
152
|
+
|
|
153
|
+
## Test harness
|
|
154
|
+
|
|
155
|
+
<!-- pulse-doc-source: examples/11-events/tests/pulse.harness.ts -->
|
|
156
|
+
```ts
|
|
157
|
+
export default { cases: [
|
|
158
|
+
{
|
|
159
|
+
name: 'http health remains separate',
|
|
160
|
+
request: { method: 'GET', path: '/health' },
|
|
161
|
+
expect: { status: 200, text: 'ok' },
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
name: 'schema event emits an accepted frame',
|
|
165
|
+
kind: 'event',
|
|
166
|
+
event: {
|
|
167
|
+
type: 'device.reading',
|
|
168
|
+
schema: 'events.DeviceReading',
|
|
169
|
+
payload: { deviceId: 'sensor-7', temperatureC: 21 },
|
|
170
|
+
},
|
|
171
|
+
expect: {
|
|
172
|
+
status: 'completed',
|
|
173
|
+
emitted: [{
|
|
174
|
+
type: 'device.reading.accepted',
|
|
175
|
+
schema: 'events.DeviceReadingAccepted',
|
|
176
|
+
payload: { deviceId: 'sensor-7', accepted: true },
|
|
177
|
+
}],
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
name: 'no-payload event emits a no-payload frame',
|
|
182
|
+
kind: 'event',
|
|
183
|
+
event: { type: 'system.tick', schema: null },
|
|
184
|
+
expect: {
|
|
185
|
+
status: 'completed',
|
|
186
|
+
emitted: [{ type: 'system.heartbeat', schema: null }],
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
] }
|
|
190
|
+
```
|
|
191
|
+
<!-- /pulse-doc-source -->
|
|
192
|
+
|
|
193
|
+
See [Static events and outbound emission](../../docs/guides/events.md) for the
|
|
194
|
+
frame, queue, target, diagnostic, and Native-extension contracts.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pulse-examples/11-events",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"doctor": "pulse doctor",
|
|
8
|
+
"inspect": "pulse inspect",
|
|
9
|
+
"test": "pulse test",
|
|
10
|
+
"dev": "pulse dev",
|
|
11
|
+
"build": "pulse build"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@pulse-compute/pulse": "1.0.0-beta.2"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@pulse-compute/cli": "1.0.0-beta.2",
|
|
18
|
+
"typescript": "5.9.3"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Pulse } from '@pulse-compute/pulse'
|
|
2
|
+
|
|
3
|
+
type DeviceReading = Readonly<{
|
|
4
|
+
deviceId: string
|
|
5
|
+
temperatureC: number
|
|
6
|
+
}>
|
|
7
|
+
|
|
8
|
+
const app = new Pulse({ auto: true })
|
|
9
|
+
|
|
10
|
+
app.get('/health', async (ctx) => ctx.text('ok'))
|
|
11
|
+
|
|
12
|
+
app.on<DeviceReading>('device.reading', { schema: 'events.DeviceReading' }, async (ctx) => {
|
|
13
|
+
const reading = ctx.event.payload
|
|
14
|
+
ctx.log.info('device reading accepted')
|
|
15
|
+
await ctx.emit('device.reading.accepted', {
|
|
16
|
+
schema: 'events.DeviceReadingAccepted',
|
|
17
|
+
payload: { deviceId: reading.deviceId, accepted: true },
|
|
18
|
+
})
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
app.on('system.tick', { schema: null }, async (ctx) => {
|
|
22
|
+
ctx.log.info('system tick accepted')
|
|
23
|
+
await ctx.emit('system.heartbeat', { schema: null })
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
export default app
|
|
27
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { defineSchemaRegistry, schema } from '@pulse-compute/pulse/schema'
|
|
2
|
+
|
|
3
|
+
export interface DeviceReading {
|
|
4
|
+
deviceId: string
|
|
5
|
+
temperatureC: number
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface DeviceReadingAccepted {
|
|
9
|
+
deviceId: string
|
|
10
|
+
accepted: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default defineSchemaRegistry({
|
|
14
|
+
schemas: {
|
|
15
|
+
'events.DeviceReading': schema<DeviceReading>(),
|
|
16
|
+
'events.DeviceReadingAccepted': schema<DeviceReadingAccepted>(),
|
|
17
|
+
},
|
|
18
|
+
})
|
|
19
|
+
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export default { cases: [
|
|
2
|
+
{
|
|
3
|
+
name: 'http health remains separate',
|
|
4
|
+
request: { method: 'GET', path: '/health' },
|
|
5
|
+
expect: { status: 200, text: 'ok' },
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
name: 'schema event emits an accepted frame',
|
|
9
|
+
kind: 'event',
|
|
10
|
+
event: {
|
|
11
|
+
type: 'device.reading',
|
|
12
|
+
schema: 'events.DeviceReading',
|
|
13
|
+
payload: { deviceId: 'sensor-7', temperatureC: 21 },
|
|
14
|
+
},
|
|
15
|
+
expect: {
|
|
16
|
+
status: 'completed',
|
|
17
|
+
emitted: [{
|
|
18
|
+
type: 'device.reading.accepted',
|
|
19
|
+
schema: 'events.DeviceReadingAccepted',
|
|
20
|
+
payload: { deviceId: 'sensor-7', accepted: true },
|
|
21
|
+
}],
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: 'no-payload event emits a no-payload frame',
|
|
26
|
+
kind: 'event',
|
|
27
|
+
event: { type: 'system.tick', schema: null },
|
|
28
|
+
expect: {
|
|
29
|
+
status: 'completed',
|
|
30
|
+
emitted: [{ type: 'system.heartbeat', schema: null }],
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
] }
|
|
34
|
+
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { defineConfig } from '@pulse-compute/pulse'
|
|
2
|
+
|
|
3
|
+
export default defineConfig((_scope) => ({
|
|
4
|
+
pulse: {
|
|
5
|
+
entry: 'src/index.ts',
|
|
6
|
+
tests: 'tests/pulse.harness.ts',
|
|
7
|
+
defaultProfile: 'local',
|
|
8
|
+
strict: true,
|
|
9
|
+
},
|
|
10
|
+
local: {
|
|
11
|
+
host: 'node',
|
|
12
|
+
target: 'native',
|
|
13
|
+
outDir: 'dist',
|
|
14
|
+
dev: {
|
|
15
|
+
fetches: {
|
|
16
|
+
'POST https://mcp.example.test/rpc': {
|
|
17
|
+
opaque: true,
|
|
18
|
+
status: 200,
|
|
19
|
+
headers: [['content-type', 'application/json']],
|
|
20
|
+
chunks: ['{"jsonrpc":"2.0","result":{},"id":1}'],
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
}))
|