@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,99 @@
1
+ import { isRecord } from "../support/normalize.js";
2
+
3
+ const ARCHITECTURE_ID_PATTERN = /^[a-z][a-z0-9_.-]*$/u;
4
+ const LOCAL_NAME_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$]*$/u;
5
+
6
+ function normalizeArchitectureId(value, label) {
7
+ const normalized = String(value || "").trim().toLowerCase();
8
+ if (!ARCHITECTURE_ID_PATTERN.test(normalized)) {
9
+ throw new TypeError(`${label} must match ${ARCHITECTURE_ID_PATTERN.toString()}.`);
10
+ }
11
+ return normalized;
12
+ }
13
+
14
+ function normalizeCapabilityMap(value, label) {
15
+ if (value == null) {
16
+ return Object.freeze({});
17
+ }
18
+ if (!isRecord(value)) {
19
+ throw new TypeError(`${label} must be an object mapping local names to capability ids.`);
20
+ }
21
+
22
+ const normalized = {};
23
+ const seenCapabilityIds = new Set();
24
+ for (const [rawLocalName, rawCapabilityId] of Object.entries(value)) {
25
+ const localName = String(rawLocalName || "").trim();
26
+ if (!LOCAL_NAME_PATTERN.test(localName)) {
27
+ throw new TypeError(`${label} local name "${localName}" must be a JavaScript identifier.`);
28
+ }
29
+ const capabilityId = normalizeArchitectureId(rawCapabilityId, `${label}.${localName}`);
30
+ if (seenCapabilityIds.has(capabilityId)) {
31
+ throw new TypeError(`${label} declares capability "${capabilityId}" more than once.`);
32
+ }
33
+ seenCapabilityIds.add(capabilityId);
34
+ normalized[localName] = capabilityId;
35
+ }
36
+
37
+ return Object.freeze(normalized);
38
+ }
39
+
40
+ function assertDistinctLocalNames(maps) {
41
+ const owners = new Map();
42
+ for (const [kind, entries] of Object.entries(maps)) {
43
+ for (const localName of Object.keys(entries)) {
44
+ if (owners.has(localName)) {
45
+ throw new TypeError(
46
+ `Provider local name "${localName}" is declared by both ${owners.get(localName)} and ${kind}.`
47
+ );
48
+ }
49
+ owners.set(localName, kind);
50
+ }
51
+ }
52
+ }
53
+
54
+ function normalizeLifecycleMethod(value, label) {
55
+ if (value == null) {
56
+ return null;
57
+ }
58
+ if (typeof value !== "function") {
59
+ throw new TypeError(`${label} must be a function when provided.`);
60
+ }
61
+ return value;
62
+ }
63
+
64
+ function defineProvider({
65
+ id,
66
+ requires = {},
67
+ optional = {},
68
+ provides = {},
69
+ setup,
70
+ boot = null,
71
+ shutdown = null
72
+ } = {}) {
73
+ const providerId = normalizeArchitectureId(id, "Provider id");
74
+ const normalizedRequires = normalizeCapabilityMap(requires, `${providerId}.requires`);
75
+ const normalizedOptional = normalizeCapabilityMap(optional, `${providerId}.optional`);
76
+ const normalizedProvides = normalizeCapabilityMap(provides, `${providerId}.provides`);
77
+ assertDistinctLocalNames({
78
+ requires: normalizedRequires,
79
+ optional: normalizedOptional,
80
+ provides: normalizedProvides
81
+ });
82
+
83
+ const setupMethod = normalizeLifecycleMethod(setup, `${providerId}.setup`);
84
+ if (!setupMethod) {
85
+ throw new TypeError(`${providerId}.setup is required.`);
86
+ }
87
+
88
+ return Object.freeze({
89
+ id: providerId,
90
+ requires: normalizedRequires,
91
+ optional: normalizedOptional,
92
+ provides: normalizedProvides,
93
+ setup: setupMethod,
94
+ boot: normalizeLifecycleMethod(boot, `${providerId}.boot`),
95
+ shutdown: normalizeLifecycleMethod(shutdown, `${providerId}.shutdown`)
96
+ });
97
+ }
98
+
99
+ export { defineProvider, normalizeArchitectureId };
@@ -0,0 +1,2 @@
1
+ export { defineProvider } from "./defineProvider.js";
2
+ export { createCapabilityRuntime } from "./runtime.js";
@@ -0,0 +1,243 @@
1
+ import { isRecord } from "../support/normalize.js";
2
+ import { defineProvider, normalizeArchitectureId } from "./defineProvider.js";
3
+
4
+ function normalizeProviderDefinition(value) {
5
+ if (!isRecord(value)) {
6
+ throw new TypeError("Capability runtime providers must be provider definitions.");
7
+ }
8
+ return defineProvider(value);
9
+ }
10
+
11
+ function normalizeInputCapabilities(value) {
12
+ if (value == null) {
13
+ return new Map();
14
+ }
15
+ if (!isRecord(value)) {
16
+ throw new TypeError("Capability runtime inputs must be an object keyed by capability id.");
17
+ }
18
+ return new Map(Object.entries(value).map(([id, capability]) => [
19
+ normalizeArchitectureId(id, "Runtime input capability id"),
20
+ capability
21
+ ]));
22
+ }
23
+
24
+ function providerCapabilityIds(provider, field) {
25
+ return Object.values(provider[field] || {});
26
+ }
27
+
28
+ function buildProviderOrder(providers, inputCapabilities) {
29
+ const normalizedProviders = providers.map(normalizeProviderDefinition);
30
+ const providersById = new Map();
31
+ const providerByCapability = new Map();
32
+
33
+ for (const provider of normalizedProviders) {
34
+ if (providersById.has(provider.id)) {
35
+ throw new Error(`Provider "${provider.id}" is duplicated.`);
36
+ }
37
+ providersById.set(provider.id, provider);
38
+
39
+ for (const capabilityId of providerCapabilityIds(provider, "provides")) {
40
+ if (inputCapabilities.has(capabilityId)) {
41
+ throw new Error(`Capability "${capabilityId}" is supplied both as a runtime input and by ${provider.id}.`);
42
+ }
43
+ if (providerByCapability.has(capabilityId)) {
44
+ throw new Error(
45
+ `Capability "${capabilityId}" is provided by both ${providerByCapability.get(capabilityId).id} and ${provider.id}.`
46
+ );
47
+ }
48
+ providerByCapability.set(capabilityId, provider);
49
+ }
50
+ }
51
+
52
+ for (const provider of normalizedProviders) {
53
+ for (const capabilityId of providerCapabilityIds(provider, "requires")) {
54
+ if (!inputCapabilities.has(capabilityId) && !providerByCapability.has(capabilityId)) {
55
+ throw new Error(`Provider "${provider.id}" requires missing capability "${capabilityId}".`);
56
+ }
57
+ if (providerByCapability.get(capabilityId)?.id === provider.id) {
58
+ throw new Error(`Provider "${provider.id}" cannot require capability "${capabilityId}" that it provides.`);
59
+ }
60
+ }
61
+ }
62
+
63
+ const visited = new Set();
64
+ const visiting = new Set();
65
+ const ordered = [];
66
+
67
+ function visit(provider, lineage = []) {
68
+ if (visited.has(provider.id)) {
69
+ return;
70
+ }
71
+ if (visiting.has(provider.id)) {
72
+ throw new Error(`Provider capability cycle detected: ${[...lineage, provider.id].join(" -> ")}.`);
73
+ }
74
+
75
+ visiting.add(provider.id);
76
+ const dependencyProviders = [
77
+ ...providerCapabilityIds(provider, "requires"),
78
+ ...providerCapabilityIds(provider, "optional")
79
+ ]
80
+ .map((capabilityId) => providerByCapability.get(capabilityId) || null)
81
+ .filter(Boolean)
82
+ .sort((left, right) => left.id.localeCompare(right.id));
83
+ for (const dependencyProvider of dependencyProviders) {
84
+ visit(dependencyProvider, [...lineage, provider.id]);
85
+ }
86
+ visiting.delete(provider.id);
87
+ visited.add(provider.id);
88
+ ordered.push(provider);
89
+ }
90
+
91
+ for (const provider of [...normalizedProviders].sort((left, right) => left.id.localeCompare(right.id))) {
92
+ visit(provider);
93
+ }
94
+ return Object.freeze(ordered);
95
+ }
96
+
97
+ function resolveProviderDependencies(provider, capabilities) {
98
+ const dependencies = {};
99
+ for (const [localName, capabilityId] of Object.entries(provider.requires)) {
100
+ if (!capabilities.has(capabilityId)) {
101
+ throw new Error(`Provider "${provider.id}" requires unavailable capability "${capabilityId}".`);
102
+ }
103
+ dependencies[localName] = capabilities.get(capabilityId);
104
+ }
105
+ for (const [localName, capabilityId] of Object.entries(provider.optional)) {
106
+ dependencies[localName] = capabilities.has(capabilityId) ? capabilities.get(capabilityId) : null;
107
+ }
108
+ return Object.freeze(dependencies);
109
+ }
110
+
111
+ function normalizeProviderOutputs(provider, value) {
112
+ const expectedNames = Object.keys(provider.provides);
113
+ if (expectedNames.length === 0) {
114
+ if (value != null && (!isRecord(value) || Object.keys(value).length > 0)) {
115
+ throw new Error(`Provider "${provider.id}" declares no capabilities but setup() returned outputs.`);
116
+ }
117
+ return Object.freeze({});
118
+ }
119
+ if (!isRecord(value)) {
120
+ throw new Error(`Provider "${provider.id}" setup() must return its declared capability outputs.`);
121
+ }
122
+
123
+ const actualNames = Object.keys(value).sort();
124
+ const sortedExpectedNames = [...expectedNames].sort();
125
+ if (
126
+ actualNames.length !== sortedExpectedNames.length ||
127
+ actualNames.some((name, index) => name !== sortedExpectedNames[index])
128
+ ) {
129
+ throw new Error(
130
+ `Provider "${provider.id}" setup() outputs must be exactly: ${sortedExpectedNames.join(", ") || "<none>"}.`
131
+ );
132
+ }
133
+ return Object.freeze({ ...value });
134
+ }
135
+
136
+ function createCapabilityRuntime({ providers = [], inputs = {}, profile = "" } = {}) {
137
+ const capabilities = normalizeInputCapabilities(inputs);
138
+ const providerOrder = buildProviderOrder(Array.isArray(providers) ? providers : [], capabilities);
139
+ const providerStates = new Map();
140
+ const normalizedProfile = String(profile || "").trim();
141
+ let lifecycleState = "created";
142
+
143
+ async function stopInitializedProviders() {
144
+ const stopped = [];
145
+ let firstError = null;
146
+ for (const provider of [...providerOrder].reverse()) {
147
+ if (!providerStates.has(provider.id)) {
148
+ continue;
149
+ }
150
+ if (provider.shutdown) {
151
+ const state = providerStates.get(provider.id);
152
+ try {
153
+ await provider.shutdown(state.dependencies, Object.freeze({
154
+ outputs: state.outputs,
155
+ profile: normalizedProfile
156
+ }));
157
+ } catch (error) {
158
+ firstError ||= error;
159
+ }
160
+ }
161
+ stopped.push(provider.id);
162
+ }
163
+ if (firstError) {
164
+ throw firstError;
165
+ }
166
+ return Object.freeze(stopped);
167
+ }
168
+
169
+ async function start() {
170
+ if (lifecycleState !== "created") {
171
+ throw new Error(`Capability runtime cannot start from state "${lifecycleState}".`);
172
+ }
173
+ lifecycleState = "starting";
174
+
175
+ try {
176
+ for (const provider of providerOrder) {
177
+ const dependencies = resolveProviderDependencies(provider, capabilities);
178
+ const outputs = normalizeProviderOutputs(
179
+ provider,
180
+ await provider.setup(dependencies, Object.freeze({ profile: normalizedProfile }))
181
+ );
182
+ for (const [localName, capabilityId] of Object.entries(provider.provides)) {
183
+ capabilities.set(capabilityId, outputs[localName]);
184
+ }
185
+ providerStates.set(provider.id, Object.freeze({ dependencies, outputs }));
186
+ }
187
+
188
+ for (const provider of providerOrder) {
189
+ if (provider.boot) {
190
+ const state = providerStates.get(provider.id);
191
+ await provider.boot(state.dependencies, Object.freeze({
192
+ outputs: state.outputs,
193
+ profile: normalizedProfile
194
+ }));
195
+ }
196
+ }
197
+
198
+ lifecycleState = "started";
199
+ return api;
200
+ } catch (error) {
201
+ lifecycleState = "failed";
202
+ try {
203
+ await stopInitializedProviders();
204
+ } catch (shutdownError) {
205
+ throw new AggregateError(
206
+ [error, shutdownError],
207
+ "Capability runtime startup failed and cleanup also failed.",
208
+ { cause: error }
209
+ );
210
+ }
211
+ throw error;
212
+ }
213
+ }
214
+
215
+ async function shutdown() {
216
+ if (lifecycleState === "stopped") {
217
+ return Object.freeze([]);
218
+ }
219
+ if (lifecycleState !== "started") {
220
+ throw new Error(`Capability runtime cannot shut down from state "${lifecycleState}".`);
221
+ }
222
+ lifecycleState = "stopping";
223
+ try {
224
+ return await stopInitializedProviders();
225
+ } finally {
226
+ lifecycleState = "stopped";
227
+ }
228
+ }
229
+
230
+ function diagnostics() {
231
+ return Object.freeze({
232
+ profile: normalizedProfile,
233
+ lifecycleState,
234
+ providerOrder: Object.freeze(providerOrder.map((provider) => provider.id)),
235
+ capabilityIds: Object.freeze([...capabilities.keys()].sort())
236
+ });
237
+ }
238
+
239
+ const api = Object.freeze({ start, shutdown, diagnostics });
240
+ return api;
241
+ }
242
+
243
+ export { createCapabilityRuntime };
@@ -0,0 +1,170 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { createCapabilityRuntime, defineProvider } from "@jskit-ai/kernel/shared/capabilities";
4
+
5
+ test("providers receive named required and optional capabilities without a service locator", async () => {
6
+ const calls = [];
7
+ const databaseProvider = defineProvider({
8
+ id: "database.mysql",
9
+ requires: { env: "runtime.env" },
10
+ provides: { database: "runtime.database" },
11
+ setup({ env }) {
12
+ calls.push(["database", env.DB_NAME]);
13
+ return { database: Object.freeze({ client: "mysql2" }) };
14
+ }
15
+ });
16
+ const booksProvider = defineProvider({
17
+ id: "feature.books",
18
+ requires: { database: "runtime.database" },
19
+ optional: { logger: "runtime.logger" },
20
+ provides: { books: "feature.books" },
21
+ setup({ database, logger }) {
22
+ calls.push(["books", database.client, logger]);
23
+ return { books: Object.freeze({ ready: true }) };
24
+ }
25
+ });
26
+
27
+ const runtime = createCapabilityRuntime({
28
+ providers: [booksProvider, databaseProvider],
29
+ inputs: {
30
+ "runtime.env": { DB_NAME: "catalogue" }
31
+ }
32
+ });
33
+ await runtime.start();
34
+
35
+ assert.deepEqual(calls, [
36
+ ["database", "catalogue"],
37
+ ["books", "mysql2", null]
38
+ ]);
39
+ assert.deepEqual(runtime.diagnostics(), {
40
+ profile: "",
41
+ lifecycleState: "started",
42
+ providerOrder: ["database.mysql", "feature.books"],
43
+ capabilityIds: ["feature.books", "runtime.database", "runtime.env"]
44
+ });
45
+ assert.equal(Object.hasOwn(runtime, "get"), false);
46
+ assert.equal(Object.hasOwn(runtime, "make"), false);
47
+ assert.equal(Object.hasOwn(runtime, "has"), false);
48
+ });
49
+
50
+ test("provider lifecycle receives only declared dependencies and its own outputs", async () => {
51
+ const calls = [];
52
+ const provider = defineProvider({
53
+ id: "feature.lifecycle",
54
+ provides: { feature: "feature.lifecycle" },
55
+ setup(dependencies, context) {
56
+ calls.push(["setup", dependencies, context.profile]);
57
+ return { feature: { ok: true } };
58
+ },
59
+ boot(dependencies, context) {
60
+ calls.push(["boot", dependencies, context.outputs.feature.ok]);
61
+ },
62
+ shutdown(dependencies, context) {
63
+ calls.push(["shutdown", dependencies, context.outputs.feature.ok]);
64
+ }
65
+ });
66
+ const runtime = createCapabilityRuntime({ providers: [provider], profile: "test" });
67
+
68
+ await runtime.start();
69
+ assert.deepEqual(await runtime.shutdown(), ["feature.lifecycle"]);
70
+ assert.deepEqual(calls, [
71
+ ["setup", {}, "test"],
72
+ ["boot", {}, true],
73
+ ["shutdown", {}, true]
74
+ ]);
75
+ });
76
+
77
+ test("capability runtime rejects missing, duplicate, cyclic, and undeclared capabilities", async () => {
78
+ assert.throws(
79
+ () => createCapabilityRuntime({
80
+ providers: [defineProvider({
81
+ id: "feature.missing",
82
+ requires: { database: "runtime.database" },
83
+ setup() {}
84
+ })]
85
+ }),
86
+ /requires missing capability "runtime\.database"/u
87
+ );
88
+
89
+ const first = defineProvider({
90
+ id: "feature.first",
91
+ provides: { value: "feature.value" },
92
+ setup() {
93
+ return { value: 1 };
94
+ }
95
+ });
96
+ const second = defineProvider({
97
+ id: "feature.second",
98
+ provides: { value: "feature.value" },
99
+ setup() {
100
+ return { value: 2 };
101
+ }
102
+ });
103
+ assert.throws(
104
+ () => createCapabilityRuntime({ providers: [first, second] }),
105
+ /provided by both feature\.first and feature\.second/u
106
+ );
107
+
108
+ const left = defineProvider({
109
+ id: "feature.left",
110
+ requires: { right: "feature.right" },
111
+ provides: { left: "feature.left" },
112
+ setup() {
113
+ return { left: true };
114
+ }
115
+ });
116
+ const right = defineProvider({
117
+ id: "feature.right",
118
+ requires: { left: "feature.left" },
119
+ provides: { right: "feature.right" },
120
+ setup() {
121
+ return { right: true };
122
+ }
123
+ });
124
+ assert.throws(
125
+ () => createCapabilityRuntime({ providers: [left, right] }),
126
+ /Provider capability cycle detected/u
127
+ );
128
+
129
+ const undeclared = defineProvider({
130
+ id: "feature.undeclared",
131
+ provides: { expected: "feature.expected" },
132
+ setup() {
133
+ return { expected: true, extra: true };
134
+ }
135
+ });
136
+ const runtime = createCapabilityRuntime({ providers: [undeclared] });
137
+ await assert.rejects(runtime.start(), /outputs must be exactly: expected/u);
138
+ });
139
+
140
+ test("failed startup shuts down initialized providers and normal shutdown is idempotent", async () => {
141
+ const calls = [];
142
+ const first = defineProvider({
143
+ id: "feature.first",
144
+ provides: { first: "feature.first" },
145
+ setup() {
146
+ calls.push("first.setup");
147
+ return { first: true };
148
+ },
149
+ shutdown() {
150
+ calls.push("first.shutdown");
151
+ }
152
+ });
153
+ const failing = defineProvider({
154
+ id: "feature.failing",
155
+ requires: { first: "feature.first" },
156
+ setup() {
157
+ calls.push("failing.setup");
158
+ throw new Error("setup failed");
159
+ }
160
+ });
161
+ const failedRuntime = createCapabilityRuntime({ providers: [failing, first] });
162
+ await assert.rejects(failedRuntime.start(), /setup failed/u);
163
+ assert.deepEqual(calls, ["first.setup", "failing.setup", "first.shutdown"]);
164
+ assert.equal(failedRuntime.diagnostics().lifecycleState, "failed");
165
+
166
+ const healthyRuntime = createCapabilityRuntime({ providers: [first] });
167
+ await healthyRuntime.start();
168
+ assert.deepEqual(await healthyRuntime.shutdown(), ["feature.first"]);
169
+ assert.deepEqual(await healthyRuntime.shutdown(), []);
170
+ });
@@ -8,17 +8,46 @@ const TEST_DIRECTORY = path.dirname(fileURLToPath(import.meta.url));
8
8
  const REPO_ROOT = path.resolve(TEST_DIRECTORY, "..", "..", "..");
9
9
 
10
10
  const BARREL_EXPECTATIONS = Object.freeze([
11
+ Object.freeze({
12
+ filePath: path.join(REPO_ROOT, "packages", "kernel", "server", "platform", "index.js"),
13
+ expectedExports: Object.freeze([
14
+ "createInstalledRuntime",
15
+ "registerSurfaceRequestConstraint",
16
+ "resolveRuntimeProfileFromSurface"
17
+ ])
18
+ }),
19
+ Object.freeze({
20
+ filePath: path.join(REPO_ROOT, "packages", "kernel", "server", "actions", "index.js"),
21
+ expectedExports: Object.freeze([
22
+ "createActionCatalogue",
23
+ "createActionProvider",
24
+ "createEntityChangedActionEvent"
25
+ ])
26
+ }),
27
+ Object.freeze({
28
+ filePath: path.join(REPO_ROOT, "packages", "kernel", "shared", "capabilities", "index.js"),
29
+ expectedExports: Object.freeze([
30
+ "createCapabilityRuntime",
31
+ "defineProvider"
32
+ ])
33
+ }),
34
+ Object.freeze({
35
+ filePath: path.join(REPO_ROOT, "packages", "kernel", "server", "features", "index.js"),
36
+ expectedExports: Object.freeze([
37
+ "defineFeature"
38
+ ])
39
+ }),
11
40
  Object.freeze({
12
41
  filePath: path.join(REPO_ROOT, "packages", "kernel", "server", "runtime", "index.js"),
13
42
  expectedExports: Object.freeze([
14
43
  "AppError",
44
+ "BootstrapProvider",
45
+ "EventProvider",
46
+ "createBootstrapRuntime",
47
+ "createEventRuntime",
15
48
  "createValidationError",
16
- "installServiceRegistrationApi",
17
49
  "parsePositiveInteger",
18
- "requireAuth",
19
- "resolveServiceRegistrations",
20
- "registerDomainEventListener",
21
- "registerBootstrapPayloadContributor"
50
+ "requireAuth"
22
51
  ])
23
52
  }),
24
53
  Object.freeze({
@@ -40,13 +69,6 @@ const BARREL_EXPECTATIONS = Object.freeze([
40
69
  "transientQueryRetryDelay"
41
70
  ])
42
71
  }),
43
- Object.freeze({
44
- filePath: path.join(REPO_ROOT, "packages", "kernel", "shared", "actions", "index.js"),
45
- expectedExports: Object.freeze([
46
- "normalizeActionDefinition",
47
- "withActionDefaults"
48
- ])
49
- }),
50
72
  Object.freeze({
51
73
  filePath: path.join(REPO_ROOT, "packages", "kernel", "shared", "validators", "index.js"),
52
74
  expectedExports: Object.freeze([
@@ -108,7 +130,8 @@ const BARREL_EXPECTATIONS = Object.freeze([
108
130
  Object.freeze({
109
131
  filePath: path.join(REPO_ROOT, "packages", "kernel", "server", "http", "index.js"),
110
132
  expectedExports: Object.freeze([
111
- "registerRouteVisibilityResolver"
133
+ "HttpProvider",
134
+ "createCapabilityHttpRuntime"
112
135
  ])
113
136
  })
114
137
  ]);
@@ -14,7 +14,9 @@ const KERNEL_PACKAGE_JSON_PATH = path.join(
14
14
  );
15
15
  const SCAN_ROOTS = [
16
16
  path.join(REPO_ROOT, "packages"),
17
- path.join(REPO_ROOT, "tooling", "create-app", "templates")
17
+ path.join(REPO_ROOT, "packages", "agent-docs", "patterns", "minimal-foundation", "example"),
18
+ path.join(REPO_ROOT, "packages", "agent-docs", "patterns", "shell-foundation", "example"),
19
+ path.join(REPO_ROOT, "packages", "agent-docs", "patterns", "feature-package", "example")
18
20
  ];
19
21
  const SCANNED_EXTENSIONS = new Set([".js", ".mjs", ".cjs", ".ts", ".tsx", ".vue"]);
20
22
  const IGNORED_DIRECTORIES = new Set(["node_modules", ".git", "dist", "coverage"]);
@@ -24,8 +26,11 @@ const EXPORTED_UNUSED_ALLOWLIST = new Set([
24
26
  "./_testable",
25
27
  // Used by generated app bootstrap code emitted from kernel client vite plugin.
26
28
  "./client/moduleBootstrap",
27
- // Intentionally retained as an explicit low-level kernel API subpath.
28
- "./shared/support/tokens",
29
+ // Public composition-root API. Feature packages normally consume it through
30
+ // defineFeature(), while infrastructure providers use it directly.
31
+ "./shared/capabilities",
32
+ // Public HTTP composition provider used when assembling a server runtime.
33
+ "./server/http",
29
34
  // Public helper API for packages that publish entity-change realtime events.
30
35
  "./server/runtime/entityChangeEvents"
31
36
  ]);