@jskit-ai/kernel 0.1.159 → 0.1.161

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 (102) hide show
  1. package/_testable/index.js +1 -3
  2. package/client/componentRegistry.js +44 -0
  3. package/client/componentRegistry.test.js +16 -0
  4. package/client/moduleBootstrap.js +54 -97
  5. package/client/moduleBootstrap.test.js +47 -32
  6. package/client/vite/clientBootstrapPlugin.test.js +2 -2
  7. package/internal/node/installedPackages.js +2 -7
  8. package/internal/node/installedPackages.test.js +26 -5
  9. package/package.json +3 -5
  10. package/server/actions/actionCatalogue.js +162 -0
  11. package/server/actions/actionEvents.js +66 -0
  12. package/server/actions/actionProvider.js +23 -0
  13. package/server/actions/index.js +3 -6
  14. package/server/features/defineFeature.js +131 -0
  15. package/server/features/defineFeature.test.js +238 -0
  16. package/server/features/index.js +1 -0
  17. package/server/http/HttpProvider.js +23 -0
  18. package/server/http/_testable/index.js +0 -1
  19. package/server/http/capabilityHttpRuntime.js +123 -0
  20. package/server/http/capabilityHttpRuntime.test.js +161 -0
  21. package/server/http/directRequestActionExecutor.js +78 -0
  22. package/server/http/index.js +2 -1
  23. package/server/http/lib/index.js +1 -13
  24. package/server/http/lib/routeRegistration.js +9 -25
  25. package/server/platform/index.js +2 -2
  26. package/server/platform/installedRuntime.js +122 -0
  27. package/server/platform/installedRuntime.test.js +103 -0
  28. package/server/platform/providerRuntime/capabilityProviderLoader.js +100 -0
  29. package/server/platform/providerRuntime/capabilityProviderLoader.test.js +80 -0
  30. package/server/platform/providerRuntime/packageCatalog.js +4 -1
  31. package/server/platform/surfaceRuntime.js +1 -15
  32. package/server/runtime/BootstrapProvider.js +69 -0
  33. package/server/runtime/EventProvider.js +74 -0
  34. package/server/runtime/EventProvider.test.js +32 -0
  35. package/server/runtime/entityChangeEvents.js +7 -7
  36. package/server/runtime/entityChangeEvents.test.js +2 -2
  37. package/server/runtime/fastifyBootstrap.js +0 -40
  38. package/server/runtime/index.js +2 -3
  39. package/server/surface/index.js +0 -1
  40. package/shared/actions/actionDefinitions.js +20 -0
  41. package/shared/actions/pipeline.js +22 -0
  42. package/shared/actions/policies.js +1 -1
  43. package/shared/actions/registry.js +3 -0
  44. package/shared/capabilities/defineProvider.js +99 -0
  45. package/shared/capabilities/index.js +2 -0
  46. package/shared/capabilities/runtime.js +243 -0
  47. package/shared/capabilities/runtime.test.js +170 -0
  48. package/test/barrelExposure.test.js +36 -13
  49. package/test/exportsContract.test.js +8 -3
  50. package/server/actions/ActionRuntimeServiceProvider.js +0 -304
  51. package/server/actions/ActionRuntimeServiceProvider.test.js +0 -558
  52. package/server/container/ContainerCoreServiceProvider.js +0 -27
  53. package/server/container/index.js +0 -10
  54. package/server/http/HttpFastifyServiceProvider.js +0 -25
  55. package/server/http/lib/httpRuntime.js +0 -83
  56. package/server/http/lib/kernel.js +0 -15
  57. package/server/http/lib/kernel.test.js +0 -1565
  58. package/server/http/lib/requestActionExecutor.js +0 -265
  59. package/server/http/lib/requestScope.js +0 -57
  60. package/server/kernel/KernelCoreServiceProvider.js +0 -27
  61. package/server/kernel/index.js +0 -10
  62. package/server/platform/PlatformServerRuntimeServiceProvider.js +0 -30
  63. package/server/platform/providerRuntime/providerLoader.js +0 -283
  64. package/server/platform/providerRuntime.js +0 -146
  65. package/server/platform/providerRuntime.test.js +0 -214
  66. package/server/platform/runtime.js +0 -40
  67. package/server/registries/actionSurfaceSourceRegistry.js +0 -148
  68. package/server/registries/bootstrapPayloadContributorRegistry.js +0 -51
  69. package/server/registries/domainEventListenerRegistry.js +0 -58
  70. package/server/registries/index.js +0 -32
  71. package/server/registries/primitives.js +0 -63
  72. package/server/registries/routeVisibilityResolverRegistry.js +0 -84
  73. package/server/registries/serviceRegistrationRegistry.js +0 -488
  74. package/server/runtime/ServerRuntimeCoreServiceProvider.js +0 -64
  75. package/server/runtime/ServerRuntimeCoreServiceProvider.test.js +0 -53
  76. package/server/runtime/apiRoutePolicyParity.test.js +0 -109
  77. package/server/runtime/apiRouteRegistration.js +0 -65
  78. package/server/runtime/bootBootstrapRoutes.js +0 -29
  79. package/server/runtime/bootBootstrapRoutes.test.js +0 -69
  80. package/server/runtime/bootstrapContributors.test.js +0 -166
  81. package/server/runtime/composition.js +0 -142
  82. package/server/runtime/domainEvents.test.js +0 -114
  83. package/server/runtime/domainRules.js +0 -50
  84. package/server/runtime/domainRules.test.js +0 -87
  85. package/server/runtime/runtimeAssembly.js +0 -113
  86. package/server/runtime/runtimeKernel.js +0 -55
  87. package/server/runtime/serviceRegistration.test.js +0 -251
  88. package/server/support/SupportCoreServiceProvider.js +0 -27
  89. package/server/surface/SurfaceRoutingServiceProvider.js +0 -27
  90. package/shared/actions/index.js +0 -2
  91. package/shared/runtime/application.js +0 -324
  92. package/shared/runtime/container.js +0 -261
  93. package/shared/runtime/containerErrors.js +0 -22
  94. package/shared/runtime/index.js +0 -18
  95. package/shared/runtime/kernelErrors.js +0 -25
  96. package/shared/runtime/kernelErrors.test.js +0 -120
  97. package/shared/runtime/serviceProvider.js +0 -13
  98. package/shared/support/containerToken.js +0 -8
  99. package/shared/support/generatedUiContract.js +0 -542
  100. package/shared/support/generatedUiContract.test.js +0 -218
  101. package/shared/support/tokens.js +0 -1
  102. package/shared/support/tokens.test.js +0 -17
@@ -0,0 +1,162 @@
1
+ import { createActionRegistry } from "../../shared/actions/registry.js";
2
+ import { isRecord } from "../../shared/support/normalize.js";
3
+
4
+ function normalizeRegistration(value) {
5
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
6
+ throw new TypeError("Action catalogue registration must be an action contributor object.");
7
+ }
8
+
9
+ return value;
10
+ }
11
+
12
+ function createActionCatalogue({
13
+ idempotencyAdapter,
14
+ auditAdapter,
15
+ observabilityAdapter,
16
+ events,
17
+ logger = null
18
+ } = {}) {
19
+ const contributors = [];
20
+ const contextContributors = [];
21
+ const contextContributorIds = new Set();
22
+ let registry = createActionRegistry({
23
+ contributors,
24
+ idempotencyAdapter,
25
+ auditAdapter,
26
+ observabilityAdapter,
27
+ events,
28
+ logger
29
+ });
30
+ let sealed = false;
31
+
32
+ function register(contributor) {
33
+ if (sealed) {
34
+ throw new Error("Action catalogue registration is closed after discovery or execution begins.");
35
+ }
36
+ const nextContributors = [...contributors, normalizeRegistration(contributor)];
37
+ const nextRegistry = createActionRegistry({
38
+ contributors: nextContributors,
39
+ idempotencyAdapter,
40
+ auditAdapter,
41
+ observabilityAdapter,
42
+ events,
43
+ logger
44
+ });
45
+
46
+ contributors.push(contributor);
47
+ registry = nextRegistry;
48
+ return api;
49
+ }
50
+
51
+ function registerContextContributor({ id, contribute } = {}) {
52
+ if (sealed) {
53
+ throw new Error("Action catalogue registration is closed after discovery or execution begins.");
54
+ }
55
+ const contributorId = String(id || "").trim();
56
+ if (!contributorId) {
57
+ throw new TypeError("Action context contributor id is required.");
58
+ }
59
+ if (typeof contribute !== "function") {
60
+ throw new TypeError(`Action context contributor "${contributorId}" requires contribute().`);
61
+ }
62
+ if (contextContributorIds.has(contributorId)) {
63
+ throw new Error(`Action context contributor "${contributorId}" is duplicated.`);
64
+ }
65
+ contextContributorIds.add(contributorId);
66
+ contextContributors.push(Object.freeze({ id: contributorId, contribute }));
67
+ return api;
68
+ }
69
+
70
+ function currentRegistry() {
71
+ sealed = true;
72
+ return registry;
73
+ }
74
+
75
+ function applyContextDefaults(context, patch) {
76
+ const next = { ...context };
77
+ for (const [key, value] of Object.entries(isRecord(patch) ? patch : {})) {
78
+ if (key === "requestMeta") {
79
+ next.requestMeta = {
80
+ ...(isRecord(value) ? value : {}),
81
+ ...(isRecord(next.requestMeta) ? next.requestMeta : {})
82
+ };
83
+ } else if (!Object.hasOwn(next, key)) {
84
+ next[key] = value;
85
+ }
86
+ }
87
+ return next;
88
+ }
89
+
90
+ async function buildExecutionContext({ actionId, version, input, context }) {
91
+ const registryInstance = currentRegistry();
92
+ const definition = registryInstance.getDefinition(actionId, version);
93
+ let enriched = { ...(isRecord(context) ? context : {}) };
94
+ for (const contributor of contextContributors) {
95
+ const patch = await contributor.contribute(Object.freeze({
96
+ actionId: definition.id,
97
+ version: definition.version,
98
+ definition,
99
+ input: Object.freeze({ ...(isRecord(input) ? input : {}) }),
100
+ context: Object.freeze({ ...enriched })
101
+ }));
102
+ enriched = applyContextDefaults(enriched, patch);
103
+ }
104
+ return Object.freeze(enriched);
105
+ }
106
+
107
+ async function execute(payload = {}) {
108
+ const source = payload && typeof payload === "object" ? payload : {};
109
+ const input = source.input && typeof source.input === "object" ? source.input : {};
110
+ const context = await buildExecutionContext({
111
+ actionId: source.actionId,
112
+ version: source.version == null ? null : source.version,
113
+ input,
114
+ context: source.context
115
+ });
116
+ return currentRegistry().execute({
117
+ actionId: source.actionId,
118
+ version: source.version == null ? null : source.version,
119
+ input,
120
+ context,
121
+ deps: isRecord(source.deps) ? source.deps : {}
122
+ });
123
+ }
124
+
125
+ async function executeStream(payload = {}) {
126
+ const source = payload && typeof payload === "object" ? payload : {};
127
+ const input = source.input && typeof source.input === "object" ? source.input : {};
128
+ const context = await buildExecutionContext({
129
+ actionId: source.actionId,
130
+ version: source.version == null ? null : source.version,
131
+ input,
132
+ context: source.context
133
+ });
134
+ return currentRegistry().executeStream({
135
+ actionId: source.actionId,
136
+ version: source.version == null ? null : source.version,
137
+ input,
138
+ context,
139
+ deps: isRecord(source.deps) ? source.deps : {}
140
+ });
141
+ }
142
+
143
+ function listDefinitions() {
144
+ return currentRegistry().listDefinitions();
145
+ }
146
+
147
+ function getDefinition(actionId, version = null) {
148
+ return currentRegistry().getDefinition(actionId, version);
149
+ }
150
+
151
+ const api = Object.freeze({
152
+ register,
153
+ registerContextContributor,
154
+ execute,
155
+ executeStream,
156
+ listDefinitions,
157
+ getDefinition
158
+ });
159
+ return api;
160
+ }
161
+
162
+ export { createActionCatalogue };
@@ -0,0 +1,66 @@
1
+ import { resolveDefaultScope } from "../runtime/entityChangeEvents.js";
2
+ import { normalizeObject, normalizeOpaqueId, normalizeText } from "../../shared/support/normalize.js";
3
+
4
+ const ENTITY_CHANGE_OPERATIONS = new Set(["created", "updated", "deleted"]);
5
+
6
+ function resolveValue(value, execution) {
7
+ return typeof value === "function" ? value(execution) : value;
8
+ }
9
+
10
+ function normalizeRealtime(value, execution) {
11
+ const source = normalizeObject(value);
12
+ if (Object.keys(source).length === 0) return null;
13
+ const event = normalizeText(source.event);
14
+ if (!event) throw new TypeError("Action entity-change realtime.event is required.");
15
+ const audience = resolveValue(source.audience ?? "event_scope", execution);
16
+ const payload = resolveValue(source.payload, execution);
17
+ if (payload != null && (!payload || typeof payload !== "object" || Array.isArray(payload))) {
18
+ throw new TypeError("Action entity-change realtime.payload must resolve to an object.");
19
+ }
20
+ return Object.freeze({
21
+ event,
22
+ audience,
23
+ ...(payload == null ? {} : { payload: Object.freeze({ ...payload }) })
24
+ });
25
+ }
26
+
27
+ function createEntityChangedActionEvent({
28
+ source,
29
+ entity,
30
+ operation,
31
+ entityId,
32
+ realtime = null
33
+ } = {}) {
34
+ const normalizedSource = normalizeText(source);
35
+ const normalizedEntity = normalizeText(entity);
36
+ if (!normalizedSource) throw new TypeError("Action entity-change source is required.");
37
+ if (!normalizedEntity) throw new TypeError("Action entity-change entity is required.");
38
+
39
+ return function buildEntityChangedEvent(execution = {}) {
40
+ const normalizedOperation = normalizeText(resolveValue(operation, execution)).toLowerCase();
41
+ if (!ENTITY_CHANGE_OPERATIONS.has(normalizedOperation)) {
42
+ throw new TypeError("Action entity-change operation must resolve to created, updated, or deleted.");
43
+ }
44
+ const normalizedEntityId = normalizeOpaqueId(resolveValue(entityId, execution));
45
+ if (normalizedEntityId == null) return null;
46
+
47
+ const context = normalizeObject(execution.context);
48
+ const requestMeta = normalizeObject(context.requestMeta);
49
+ const realtimeMeta = normalizeRealtime(realtime, execution);
50
+ return Object.freeze({
51
+ type: "entity.changed",
52
+ source: normalizedSource,
53
+ entity: normalizedEntity,
54
+ operation: normalizedOperation,
55
+ entityId: normalizedEntityId,
56
+ scope: resolveDefaultScope(normalizeObject(context.visibilityContext), { context }),
57
+ actorId: normalizeOpaqueId(context?.actor?.id),
58
+ commandId: normalizeText(requestMeta.commandId || requestMeta.idempotencyKey) || null,
59
+ sourceClientId: normalizeText(requestMeta.sourceClientId) || null,
60
+ occurredAt: new Date().toISOString(),
61
+ ...(realtimeMeta ? { realtime: realtimeMeta } : {})
62
+ });
63
+ };
64
+ }
65
+
66
+ export { createEntityChangedActionEvent };
@@ -0,0 +1,23 @@
1
+ import { defineProvider } from "../../shared/capabilities/defineProvider.js";
2
+ import { createEventRuntime } from "../runtime/EventProvider.js";
3
+ import { createActionCatalogue } from "./actionCatalogue.js";
4
+
5
+ function createActionProvider(options = {}) {
6
+ return defineProvider({
7
+ id: "runtime.actions",
8
+ optional: {
9
+ events: "runtime.events"
10
+ },
11
+ provides: {
12
+ actions: "runtime.actions"
13
+ },
14
+ setup({ events }) {
15
+ return { actions: createActionCatalogue({
16
+ ...options,
17
+ events: events || createEventRuntime()
18
+ }) };
19
+ }
20
+ });
21
+ }
22
+
23
+ export { createActionProvider };
@@ -1,6 +1,3 @@
1
- export {
2
- resolveActionContributors,
3
- resolveActionContextContributors,
4
- registerActionContextContributor,
5
- ActionRuntimeServiceProvider
6
- } from "./ActionRuntimeServiceProvider.js";
1
+ export { createActionCatalogue } from "./actionCatalogue.js";
2
+ export { createActionProvider } from "./actionProvider.js";
3
+ export { createEntityChangedActionEvent } from "./actionEvents.js";
@@ -0,0 +1,131 @@
1
+ import { isRecord } from "../../shared/support/normalize.js";
2
+ import { defineProvider } from "../../shared/capabilities/defineProvider.js";
3
+
4
+ function exactOutputNames(value, expectedNames, featureId) {
5
+ const outputs = value == null ? {} : value;
6
+ if (!isRecord(outputs)) {
7
+ throw new TypeError(`Feature "${featureId}" setup() must return its declared capability outputs.`);
8
+ }
9
+
10
+ const actualNames = Object.keys(outputs).sort();
11
+ const normalizedExpectedNames = [...expectedNames].sort();
12
+ if (
13
+ actualNames.length !== normalizedExpectedNames.length ||
14
+ actualNames.some((name, index) => name !== normalizedExpectedNames[index])
15
+ ) {
16
+ throw new TypeError(
17
+ `Feature "${featureId}" setup() outputs must be exactly: ${normalizedExpectedNames.join(", ") || "<none>"}.`
18
+ );
19
+ }
20
+
21
+ return Object.freeze({ ...outputs });
22
+ }
23
+
24
+ function normalizeActionFactory(value, featureId) {
25
+ if (value == null) {
26
+ return () => [];
27
+ }
28
+ if (typeof value === "function") {
29
+ return value;
30
+ }
31
+ if (Array.isArray(value)) {
32
+ return () => value;
33
+ }
34
+ throw new TypeError(`Feature "${featureId}" actions must be an array or function.`);
35
+ }
36
+
37
+ function applyActionDefaults(actions, defaults, featureId) {
38
+ if (!isRecord(defaults)) {
39
+ throw new TypeError(`Feature "${featureId}" actionDefaults must be an object.`);
40
+ }
41
+ if (Object.hasOwn(defaults, "dependencies")) {
42
+ throw new TypeError(`Feature "${featureId}" actionDefaults cannot declare container dependencies.`);
43
+ }
44
+
45
+ return actions.map((value) => {
46
+ if (!isRecord(value)) {
47
+ throw new TypeError(`Feature "${featureId}" actions must contain action definition objects.`);
48
+ }
49
+ if (Object.hasOwn(value, "dependencies")) {
50
+ throw new TypeError(`Feature "${featureId}" actions cannot declare container dependencies.`);
51
+ }
52
+ return Object.freeze({ ...defaults, ...value });
53
+ });
54
+ }
55
+
56
+ function withoutActionCatalogue(dependencies) {
57
+ const { actionCatalogue, ...featureDependencies } = dependencies;
58
+ return Object.freeze(featureDependencies);
59
+ }
60
+
61
+ function defineFeature({
62
+ id,
63
+ domain = id,
64
+ requires = {},
65
+ optional = {},
66
+ provides = {},
67
+ setup = () => ({}),
68
+ actions = [],
69
+ actionDefaults = {},
70
+ boot = null,
71
+ shutdown = null
72
+ } = {}) {
73
+ if (Object.hasOwn(requires || {}, "actionCatalogue") || Object.hasOwn(optional || {}, "actionCatalogue")) {
74
+ throw new TypeError("Feature dependencies reserve the local name actionCatalogue.");
75
+ }
76
+ if (typeof setup !== "function") {
77
+ throw new TypeError(`Feature "${String(id || "")}" setup must be a function.`);
78
+ }
79
+
80
+ const actionFactory = normalizeActionFactory(actions, String(id || ""));
81
+ const expectedOutputNames = Object.keys(provides || {});
82
+
83
+ return defineProvider({
84
+ id,
85
+ requires: {
86
+ ...requires,
87
+ actionCatalogue: "runtime.actions"
88
+ },
89
+ optional,
90
+ provides,
91
+ async setup(dependencies, context) {
92
+ const { actionCatalogue } = dependencies;
93
+ const featureDependencies = withoutActionCatalogue(dependencies);
94
+ const featureContext = Object.freeze({ ...context, actionCatalogue });
95
+ const outputs = exactOutputNames(
96
+ await setup(Object.freeze(featureDependencies), featureContext),
97
+ expectedOutputNames,
98
+ String(id || "")
99
+ );
100
+ const featureActions = await actionFactory(
101
+ Object.freeze({
102
+ ...featureDependencies,
103
+ ...outputs
104
+ }),
105
+ featureContext
106
+ );
107
+ if (!Array.isArray(featureActions)) {
108
+ throw new TypeError(`Feature "${String(id || "")}" actions() must return an array.`);
109
+ }
110
+
111
+ const materializedActions = applyActionDefaults(featureActions, actionDefaults, String(id || ""));
112
+
113
+ if (materializedActions.length > 0) {
114
+ actionCatalogue.register({
115
+ contributorId: String(id || ""),
116
+ domain,
117
+ actions: materializedActions
118
+ });
119
+ }
120
+ return outputs;
121
+ },
122
+ boot: typeof boot === "function"
123
+ ? (dependencies, context) => boot(withoutActionCatalogue(dependencies), context)
124
+ : boot,
125
+ shutdown: typeof shutdown === "function"
126
+ ? (dependencies, context) => shutdown(withoutActionCatalogue(dependencies), context)
127
+ : shutdown
128
+ });
129
+ }
130
+
131
+ export { defineFeature };
@@ -0,0 +1,238 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { createSchema } from "json-rest-schema";
4
+ import {
5
+ createActionProvider,
6
+ createEntityChangedActionEvent
7
+ } from "@jskit-ai/kernel/server/actions";
8
+ import { createCapabilityRuntime, defineProvider } from "@jskit-ai/kernel/shared/capabilities";
9
+ import { defineFeature } from "@jskit-ai/kernel/server/features";
10
+ import { EventProvider } from "@jskit-ai/kernel/server/runtime";
11
+
12
+ function emptyInput() {
13
+ return { schema: createSchema({}) };
14
+ }
15
+
16
+ test("features capture only named capabilities and publish actions without container lookup", async () => {
17
+ let setupActionCatalogue = null;
18
+ const database = Object.freeze({
19
+ async listBooks() {
20
+ return [{ id: 1, title: "Kindred" }];
21
+ }
22
+ });
23
+ const databaseProvider = defineProvider({
24
+ id: "database.test",
25
+ provides: { database: "runtime.database" },
26
+ setup() {
27
+ return { database };
28
+ }
29
+ });
30
+ const booksFeature = defineFeature({
31
+ id: "feature.books",
32
+ domain: "books",
33
+ requires: { database: "runtime.database" },
34
+ provides: { books: "feature.books" },
35
+ actionDefaults: {
36
+ channels: ["api", "assistant"],
37
+ surfaces: ["app"]
38
+ },
39
+ setup({ database: selectedDatabase }, { actionCatalogue }) {
40
+ setupActionCatalogue = actionCatalogue;
41
+ return {
42
+ books: Object.freeze({
43
+ list: () => selectedDatabase.listBooks()
44
+ })
45
+ };
46
+ },
47
+ actions({ books }) {
48
+ return [{
49
+ id: "books.list",
50
+ kind: "query",
51
+ input: emptyInput(),
52
+ idempotency: "none",
53
+ async execute() {
54
+ return books.list();
55
+ }
56
+ }];
57
+ }
58
+ });
59
+
60
+ let actions = null;
61
+ const observer = defineProvider({
62
+ id: "test.observer",
63
+ requires: { actionCatalogue: "runtime.actions" },
64
+ setup({ actionCatalogue }) {
65
+ actions = actionCatalogue;
66
+ }
67
+ });
68
+ const runtime = createCapabilityRuntime({
69
+ providers: [observer, booksFeature, createActionProvider(), databaseProvider]
70
+ });
71
+ await runtime.start();
72
+
73
+ assert.deepEqual(actions.listDefinitions().map((entry) => entry.id), ["books.list"]);
74
+ assert.deepEqual(
75
+ await actions.execute({
76
+ actionId: "books.list",
77
+ context: { channel: "assistant", surface: "app" }
78
+ }),
79
+ [{ id: 1, title: "Kindred" }]
80
+ );
81
+ assert.equal(Object.hasOwn(actions, "make"), false);
82
+ assert.equal(Object.hasOwn(actions, "resolve"), false);
83
+ assert.equal(setupActionCatalogue, actions);
84
+ assert.throws(
85
+ () => actions.register({ contributorId: "late", domain: "late", actions: [] }),
86
+ /registration is closed/u
87
+ );
88
+ });
89
+
90
+ test("feature registration rejects duplicate actions and undeclared setup outputs", async () => {
91
+ const action = {
92
+ id: "books.read",
93
+ kind: "query",
94
+ channels: ["api"],
95
+ surfaces: ["app"],
96
+ input: emptyInput(),
97
+ idempotency: "none",
98
+ async execute() {
99
+ return {};
100
+ }
101
+ };
102
+ const first = defineFeature({ id: "feature.first", domain: "books", actions: [action] });
103
+ const second = defineFeature({ id: "feature.second", domain: "books", actions: [action] });
104
+ const duplicateRuntime = createCapabilityRuntime({
105
+ providers: [createActionProvider(), first, second]
106
+ });
107
+ await assert.rejects(duplicateRuntime.start(), /duplicated between contributors/u);
108
+
109
+ const undeclared = defineFeature({
110
+ id: "feature.undeclared",
111
+ setup() {
112
+ return { surprise: true };
113
+ }
114
+ });
115
+ const invalidRuntime = createCapabilityRuntime({
116
+ providers: [createActionProvider(), undeclared]
117
+ });
118
+ await assert.rejects(invalidRuntime.start(), /outputs must be exactly: <none>/u);
119
+ });
120
+
121
+ test("action context contributors enrich execution without a request scope container", async () => {
122
+ let actions;
123
+ const actionProvider = createActionProvider();
124
+ const feature = defineFeature({
125
+ id: "feature.context-proof",
126
+ domain: "context-proof",
127
+ actions: [{
128
+ id: "context-proof.read",
129
+ kind: "query",
130
+ channels: ["api"],
131
+ surfaces: ["app"],
132
+ input: emptyInput(),
133
+ idempotency: "none",
134
+ async execute(_input, context) {
135
+ return context;
136
+ }
137
+ }]
138
+ });
139
+ const contributor = defineProvider({
140
+ id: "test.context-contributor",
141
+ requires: { actionCatalogue: "runtime.actions" },
142
+ setup({ actionCatalogue }) {
143
+ actions = actionCatalogue;
144
+ actionCatalogue.registerContextContributor({
145
+ id: "test.actor",
146
+ contribute({ context }) {
147
+ return {
148
+ actorId: context.requestMeta?.request?.actorId || "anonymous",
149
+ requestMeta: { contributed: true }
150
+ };
151
+ }
152
+ });
153
+ return {};
154
+ }
155
+ });
156
+ const runtime = createCapabilityRuntime({ providers: [actionProvider, contributor, feature] });
157
+
158
+ await runtime.start();
159
+ const context = await actions.execute({
160
+ actionId: "context-proof.read",
161
+ context: {
162
+ channel: "api",
163
+ surface: "app",
164
+ requestMeta: {
165
+ request: { actorId: "user-1" },
166
+ callerOwned: true
167
+ }
168
+ }
169
+ });
170
+
171
+ assert.equal(context.actorId, "user-1");
172
+ assert.equal(context.requestMeta.contributed, true);
173
+ assert.deepEqual(context.requestMeta.request, { actorId: "user-1" });
174
+ assert.equal(context.requestMeta.callerOwned, true);
175
+ assert.throws(
176
+ () => actions.registerContextContributor({ id: "late", contribute() {} }),
177
+ /registration is closed/u
178
+ );
179
+ await runtime.shutdown();
180
+ });
181
+
182
+ test("feature actions publish explicit success events without service registrations", async () => {
183
+ let actions;
184
+ const received = [];
185
+ const feature = defineFeature({
186
+ id: "feature.event-proof",
187
+ domain: "books",
188
+ actions: [{
189
+ id: "books.save",
190
+ kind: "command",
191
+ channels: ["api"],
192
+ surfaces: ["app"],
193
+ input: emptyInput(),
194
+ idempotency: "none",
195
+ events: [createEntityChangedActionEvent({
196
+ source: "books",
197
+ entity: "book",
198
+ operation: "updated",
199
+ entityId: ({ context }) => context.actor.id,
200
+ realtime: {
201
+ event: "books.changed",
202
+ audience: "actor_user"
203
+ }
204
+ })],
205
+ async execute() {
206
+ return { saved: true };
207
+ }
208
+ }]
209
+ });
210
+ const listener = defineProvider({
211
+ id: "test.event-listener",
212
+ requires: {
213
+ actionCatalogue: "runtime.actions",
214
+ events: "runtime.events"
215
+ },
216
+ setup({ actionCatalogue, events }) {
217
+ actions = actionCatalogue;
218
+ events.register({ id: "test.capture", handle: (event) => received.push(event) });
219
+ return {};
220
+ }
221
+ });
222
+ const runtime = createCapabilityRuntime({
223
+ providers: [EventProvider, createActionProvider(), feature, listener]
224
+ });
225
+ await runtime.start();
226
+
227
+ await actions.execute({
228
+ actionId: "books.save",
229
+ input: {},
230
+ context: { channel: "api", surface: "app", actor: { id: "7" } }
231
+ });
232
+
233
+ assert.equal(received.length, 1);
234
+ assert.equal(received[0].type, "entity.changed");
235
+ assert.equal(received[0].entityId, "7");
236
+ assert.equal(received[0].realtime.event, "books.changed");
237
+ assert.equal(received[0].realtime.audience, "actor_user");
238
+ });
@@ -0,0 +1 @@
1
+ export { defineFeature } from "./defineFeature.js";
@@ -0,0 +1,23 @@
1
+ import { defineProvider } from "../../shared/capabilities/defineProvider.js";
2
+ import { createCapabilityHttpRuntime } from "./capabilityHttpRuntime.js";
3
+
4
+ const HttpProvider = defineProvider({
5
+ id: "runtime.http",
6
+ requires: {
7
+ actions: "runtime.actions",
8
+ fastify: "runtime.fastify"
9
+ },
10
+ provides: {
11
+ http: "runtime.http"
12
+ },
13
+ setup({ actions, fastify }) {
14
+ return {
15
+ http: createCapabilityHttpRuntime({ actions, fastify })
16
+ };
17
+ },
18
+ boot(_dependencies, { outputs }) {
19
+ outputs.http.start();
20
+ }
21
+ });
22
+
23
+ export { HttpProvider };
@@ -1,2 +1 @@
1
- export { createHttpRuntime } from "../lib/httpRuntime.js";
2
1
  export { compileRouteValidator } from "../lib/routeValidator.js";