@capekai/core 1.0.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 (148) hide show
  1. package/README.md +12 -0
  2. package/package.json +105 -0
  3. package/src/adapters/ai-sdk.ts +84 -0
  4. package/src/compaction/contracts.ts +82 -0
  5. package/src/compaction/executor.ts +161 -0
  6. package/src/compaction/policy.ts +318 -0
  7. package/src/compaction/recovery.ts +139 -0
  8. package/src/compaction/task.ts +540 -0
  9. package/src/configuration/contracts.ts +58 -0
  10. package/src/configuration/defaults.ts +27 -0
  11. package/src/configuration/runtime.ts +42 -0
  12. package/src/configuration/single-model.ts +75 -0
  13. package/src/context/assembler.ts +112 -0
  14. package/src/context/index.ts +2 -0
  15. package/src/context/sources.ts +119 -0
  16. package/src/context/workspace.ts +63 -0
  17. package/src/core/agent.ts +401 -0
  18. package/src/core/build-tools.ts +139 -0
  19. package/src/core/chat-handler.ts +858 -0
  20. package/src/core/error-handling.ts +18 -0
  21. package/src/core/fork.ts +103 -0
  22. package/src/core/interrupt.ts +192 -0
  23. package/src/core/message-utils.ts +261 -0
  24. package/src/core/model-utils.ts +149 -0
  25. package/src/core/part-utils.ts +88 -0
  26. package/src/core/provider-utils.ts +67 -0
  27. package/src/core/revert.ts +46 -0
  28. package/src/core/step-handlers.ts +157 -0
  29. package/src/core/stream/finalization.ts +65 -0
  30. package/src/core/stream/stream-config.ts +82 -0
  31. package/src/core/stream-handlers.ts +242 -0
  32. package/src/core/structured-output.ts +68 -0
  33. package/src/core/tool-builders/agent-tools.ts +71 -0
  34. package/src/core/tool-builders/external-tools.ts +179 -0
  35. package/src/core/tool-builders/types.ts +16 -0
  36. package/src/core/tool-builders/workspace-tools.ts +293 -0
  37. package/src/core/tool-capabilities.ts +65 -0
  38. package/src/goals/evaluator.ts +171 -0
  39. package/src/goals/index.ts +3 -0
  40. package/src/goals/loop.ts +167 -0
  41. package/src/goals/service.ts +39 -0
  42. package/src/index.ts +10 -0
  43. package/src/internal/ask-authority.ts +29 -0
  44. package/src/internal/composition.ts +44 -0
  45. package/src/internal/configuration.ts +22 -0
  46. package/src/internal/execution.ts +108 -0
  47. package/src/internal/hosts.ts +64 -0
  48. package/src/internal/plugins.ts +71 -0
  49. package/src/internal/providers.ts +32 -0
  50. package/src/internal/sandbox.ts +19 -0
  51. package/src/internal/tools.ts +48 -0
  52. package/src/internal/workspace.ts +25 -0
  53. package/src/kernel/diagnostics.ts +249 -0
  54. package/src/kernel/errors.ts +120 -0
  55. package/src/kernel/events.ts +82 -0
  56. package/src/kernel/index.ts +72 -0
  57. package/src/kernel/kernel.ts +62 -0
  58. package/src/kernel/lifecycle.ts +72 -0
  59. package/src/kernel/plugin.ts +218 -0
  60. package/src/kernel/registry.ts +493 -0
  61. package/src/kernel/scope.ts +776 -0
  62. package/src/kernel/service-key.ts +19 -0
  63. package/src/kernel/types.ts +317 -0
  64. package/src/memory/index.ts +2 -0
  65. package/src/memory/memory-tool.ts +75 -0
  66. package/src/memory/registry.ts +172 -0
  67. package/src/permission/ask-user-api.ts +70 -0
  68. package/src/permission/contracts.ts +135 -0
  69. package/src/permission/permission-request-manager.ts +58 -0
  70. package/src/permission/policy.ts +277 -0
  71. package/src/permission/runtime.ts +612 -0
  72. package/src/plugins/compaction-policy.ts +46 -0
  73. package/src/plugins/compose.ts +171 -0
  74. package/src/plugins/context-sections.ts +246 -0
  75. package/src/plugins/default-agent-driver.ts +14 -0
  76. package/src/plugins/facade-plugins.ts +129 -0
  77. package/src/plugins/goal-domain.ts +82 -0
  78. package/src/plugins/legacy-system-message.ts +152 -0
  79. package/src/plugins/loaded-tools.ts +23 -0
  80. package/src/plugins/memory-domain.ts +264 -0
  81. package/src/plugins/orchestrator-session.ts +29 -0
  82. package/src/plugins/permission-policy.ts +49 -0
  83. package/src/plugins/retry-policy.ts +28 -0
  84. package/src/plugins/scheduler-domain.ts +192 -0
  85. package/src/plugins/service-keys.ts +294 -0
  86. package/src/plugins/session-search-domain.ts +238 -0
  87. package/src/plugins/skills-domain.ts +272 -0
  88. package/src/plugins/subagent-domain.ts +287 -0
  89. package/src/plugins/tool-catalog.ts +78 -0
  90. package/src/plugins/tool-output-policy.ts +52 -0
  91. package/src/plugins/value-plugins.ts +150 -0
  92. package/src/plugins/workflow-domain.ts +198 -0
  93. package/src/plugins/workspace-policy.ts +37 -0
  94. package/src/providers/registry.ts +63 -0
  95. package/src/providers/types.ts +44 -0
  96. package/src/retry/policy.ts +282 -0
  97. package/src/retry/stream-chat.ts +312 -0
  98. package/src/runtime/agent-runtime.ts +83 -0
  99. package/src/runtime/default-agent-driver.ts +23 -0
  100. package/src/runtime/domain-tool-source.ts +156 -0
  101. package/src/runtime/events.ts +61 -0
  102. package/src/runtime/host-dependencies.ts +71 -0
  103. package/src/runtime/host-guidance.ts +22 -0
  104. package/src/runtime/host-layout.ts +23 -0
  105. package/src/runtime/host.ts +129 -0
  106. package/src/runtime/standalone-host.ts +118 -0
  107. package/src/sandbox/controller.ts +204 -0
  108. package/src/sandbox/model.ts +305 -0
  109. package/src/sandbox/provider.ts +53 -0
  110. package/src/sandbox/types.ts +110 -0
  111. package/src/scheduler/host.ts +22 -0
  112. package/src/scheduler/scheduler-tool.ts +172 -0
  113. package/src/session-search/host.ts +56 -0
  114. package/src/session-search/index.ts +23 -0
  115. package/src/session-search/session-search-tool.ts +151 -0
  116. package/src/skills/index.ts +3 -0
  117. package/src/skills/registry.ts +63 -0
  118. package/src/skills/skill-manage-tool.ts +205 -0
  119. package/src/skills/skill-tool.ts +42 -0
  120. package/src/storage/contracts.ts +159 -0
  121. package/src/storage/memory.ts +321 -0
  122. package/src/storage/options.ts +75 -0
  123. package/src/storage/runtime.ts +115 -0
  124. package/src/storage/sqlite-tool-output-artifacts.ts +106 -0
  125. package/src/storage/sqlite.ts +321 -0
  126. package/src/storage/tool-output-artifacts.ts +75 -0
  127. package/src/storage.ts +31 -0
  128. package/src/subagent/child-session.ts +282 -0
  129. package/src/subagent/guidance.ts +8 -0
  130. package/src/subagent/policy.ts +198 -0
  131. package/src/subagent/task-tool.ts +584 -0
  132. package/src/tool-output/contracts.ts +111 -0
  133. package/src/tool-output/policy.ts +410 -0
  134. package/src/tool.ts +1 -0
  135. package/src/tools/executor.ts +258 -0
  136. package/src/tools/install-manifest.ts +40 -0
  137. package/src/tools/llm-api.ts +77 -0
  138. package/src/tools/registry.ts +206 -0
  139. package/src/tools/tool-artifact.ts +182 -0
  140. package/src/tools/tool-source.ts +53 -0
  141. package/src/utils/errors.ts +334 -0
  142. package/src/utils/strip-visualization.ts +50 -0
  143. package/src/workflow/decomposer.ts +139 -0
  144. package/src/workflow/execution.ts +523 -0
  145. package/src/workflow/orchestrator-session.ts +161 -0
  146. package/src/workflow/synthesizer.ts +130 -0
  147. package/src/workspace/contracts.ts +135 -0
  148. package/src/workspace/policy.ts +327 -0
@@ -0,0 +1,218 @@
1
+ /**
2
+ * Plugin records and the setup-time context handed to plugins. Every
3
+ * registration returns a disposer owned by the installing scope; context
4
+ * collects them so rollback and disposal run in reverse order.
5
+ */
6
+
7
+ import {
8
+ MalformedPluginError,
9
+ MissingDependencyError,
10
+ ScopeValidationError,
11
+ } from './errors';
12
+ import { validateListener } from './events';
13
+ import type {
14
+ CapekPlugin,
15
+ CleanupBarrier,
16
+ CompositionDiagnostics,
17
+ ContextSectionContribution,
18
+ Disposable,
19
+ EffectiveTool,
20
+ EventListenerContribution,
21
+ PluginContext,
22
+ ProvidedContextSection,
23
+ RuntimeScope,
24
+ ScopeDiagnosticsSnapshot,
25
+ ServiceKey,
26
+ ToolContribution,
27
+ } from './types';
28
+
29
+ /** The slice of a scope the plugin context is allowed to touch. */
30
+ export interface PluginContextHost {
31
+ readonly kind: RuntimeScope;
32
+ readonly scopeId: string;
33
+ registerService(
34
+ pluginId: string,
35
+ key: ServiceKey<unknown>,
36
+ value: unknown,
37
+ replacesProvider?: string,
38
+ ): Disposable;
39
+ registerTool(pluginId: string, contribution: ToolContribution): Disposable;
40
+ registerContextSection(
41
+ pluginId: string,
42
+ contribution: ContextSectionContribution,
43
+ ): Disposable;
44
+ registerListener(pluginId: string, contribution: EventListenerContribution): Disposable;
45
+ registerBarrier(pluginId: string, barrier: CleanupBarrier): Disposable;
46
+ resolveService<T>(key: ServiceKey<T>): T | undefined;
47
+ listTools(): readonly EffectiveTool[];
48
+ buildContext<TData = unknown>(data?: TData): Promise<readonly ProvidedContextSection[]>;
49
+ snapshot(): ScopeDiagnosticsSnapshot;
50
+ }
51
+
52
+ export interface PluginRecord {
53
+ readonly id: string;
54
+ readonly version?: string;
55
+ readonly scope: RuntimeScope;
56
+ readonly plugin: CapekPlugin<unknown>;
57
+ readonly context: PluginContextImpl;
58
+ status: 'pending' | 'active' | 'failed' | 'disposed';
59
+ returnedDisposable: Disposable | undefined;
60
+ }
61
+
62
+ export class PluginContextImpl implements PluginContext {
63
+ readonly disposers: Disposable[] = [];
64
+ private readonly providedIds = new Set<string>();
65
+ private readonly declaredProvides: ReadonlySet<string>;
66
+
67
+ constructor(
68
+ private readonly host: PluginContextHost,
69
+ private readonly plugin: CapekPlugin<unknown>,
70
+ ) {
71
+ this.declaredProvides = new Set((plugin.provides ?? []).map((key) => key.id));
72
+ }
73
+
74
+ get kind(): RuntimeScope {
75
+ return this.host.kind;
76
+ }
77
+
78
+ get scopeId(): string {
79
+ return this.host.scopeId;
80
+ }
81
+
82
+ get diagnostics(): CompositionDiagnostics {
83
+ return { snapshot: () => this.host.snapshot() };
84
+ }
85
+
86
+ provide<T>(key: ServiceKey<T>, service: T): Disposable {
87
+ if (key.scope !== this.host.kind) {
88
+ throw new ScopeValidationError(
89
+ `plugin '${this.plugin.id}' cannot provide service '${key.id}' with scope '${key.scope}' from a '${this.host.kind}' scope`,
90
+ );
91
+ }
92
+ if (!this.declaredProvides.has(key.id)) {
93
+ throw new MalformedPluginError(
94
+ `plugin '${this.plugin.id}' provided service '${key.id}' without declaring it in provides`,
95
+ );
96
+ }
97
+ if (this.providedIds.has(key.id)) {
98
+ throw new MalformedPluginError(
99
+ `plugin '${this.plugin.id}' provided service '${key.id}' more than once during setup`,
100
+ );
101
+ }
102
+ this.providedIds.add(key.id);
103
+ const replacesProvider = (this.plugin.overrides ?? [])
104
+ .find((override) => override.key.id === key.id)?.replacedProvider;
105
+ const registration = this.host.registerService(
106
+ this.plugin.id,
107
+ key,
108
+ service,
109
+ replacesProvider,
110
+ );
111
+ let removed = false;
112
+ const tracked: Disposable = {
113
+ dispose: () => {
114
+ if (removed) return;
115
+ removed = true;
116
+ this.providedIds.delete(key.id);
117
+ registration.dispose();
118
+ },
119
+ };
120
+ this.disposers.push(tracked);
121
+ return tracked;
122
+ }
123
+
124
+ require<T>(key: ServiceKey<T>): T {
125
+ const resolved = this.host.resolveService(key);
126
+ if (resolved === undefined) {
127
+ throw new MissingDependencyError(
128
+ `plugin '${this.plugin.id}' requires service '${key.id}' which is not available in the current scope chain`,
129
+ );
130
+ }
131
+ return resolved;
132
+ }
133
+
134
+ optional<T>(key: ServiceKey<T>): T | undefined {
135
+ return this.host.resolveService(key);
136
+ }
137
+
138
+ contributeTool(contribution: ToolContribution): Disposable {
139
+ const registration = this.host.registerTool(this.plugin.id, contribution);
140
+ this.disposers.push(registration);
141
+ return registration;
142
+ }
143
+
144
+ contributeContext(contribution: ContextSectionContribution): Disposable {
145
+ const registration = this.host.registerContextSection(this.plugin.id, contribution);
146
+ this.disposers.push(registration);
147
+ return registration;
148
+ }
149
+
150
+ listTools(): readonly EffectiveTool[] {
151
+ return this.host.listTools();
152
+ }
153
+
154
+ buildContext<TData = unknown>(data?: TData): Promise<readonly ProvidedContextSection[]> {
155
+ return this.host.buildContext(data);
156
+ }
157
+
158
+ contributeListener(contribution: EventListenerContribution): Disposable {
159
+ validateListener(contribution);
160
+ const registration = this.host.registerListener(this.plugin.id, contribution);
161
+ this.disposers.push(registration);
162
+ return registration;
163
+ }
164
+
165
+
166
+ registerCleanupBarrier(barrier: CleanupBarrier): Disposable {
167
+ const registration = this.host.registerBarrier(this.plugin.id, barrier);
168
+ this.disposers.push(registration);
169
+ return registration;
170
+ }
171
+
172
+ get providedKeyIds(): ReadonlySet<string> {
173
+ return this.providedIds;
174
+ }
175
+ }
176
+
177
+ export function createPluginRecords(
178
+ host: PluginContextHost,
179
+ plugins: readonly CapekPlugin<unknown>[],
180
+ ): Map<string, PluginRecord> {
181
+ const records = new Map<string, PluginRecord>();
182
+ for (const plugin of plugins) {
183
+ records.set(plugin.id, {
184
+ id: plugin.id,
185
+ version: plugin.version,
186
+ scope: plugin.scope,
187
+ plugin,
188
+ context: new PluginContextImpl(host, plugin),
189
+ status: 'pending',
190
+ returnedDisposable: undefined,
191
+ });
192
+ }
193
+ return records;
194
+ }
195
+
196
+ export function isDisposable(value: unknown): value is Disposable {
197
+ return typeof value === 'object' && value !== null && 'dispose' in value;
198
+ }
199
+
200
+ /** A plugin must provide exactly the services it declared. */
201
+ export function enforceDeclaredProvides(record: PluginRecord): void {
202
+ const declared = new Set((record.plugin.provides ?? []).map((key) => key.id));
203
+ const actual = record.context.providedKeyIds;
204
+ for (const id of actual) {
205
+ if (!declared.has(id)) {
206
+ throw new MalformedPluginError(
207
+ `plugin '${record.id}' provided service '${id}' without declaring it in provides`,
208
+ );
209
+ }
210
+ }
211
+ for (const id of declared) {
212
+ if (!actual.has(id)) {
213
+ throw new MalformedPluginError(
214
+ `plugin '${record.id}' declared service '${id}' but did not provide it during setup`,
215
+ );
216
+ }
217
+ }
218
+ }
@@ -0,0 +1,493 @@
1
+ /**
2
+ * Static composition planning. Before any plugin setup runs, the kernel
3
+ * validates scope placement, declared services, overrides, and dependency
4
+ * edges, then derives a deterministic activation order from the dependency
5
+ * graph with plugin-id tie-breaks. Cycles are reported with plugin and
6
+ * service identifiers.
7
+ */
8
+
9
+ import {
10
+ DependencyCycleError,
11
+ DuplicatePluginError,
12
+ DuplicateProviderError,
13
+ InvalidOverrideError,
14
+ MalformedPluginError,
15
+ MissingDependencyError,
16
+ ScopeValidationError,
17
+ ServiceCollisionError,
18
+ } from './errors';
19
+ import type {
20
+ CapekPlugin,
21
+ RuntimeScope,
22
+ ServiceKey,
23
+ ServiceOverride,
24
+ } from './types';
25
+
26
+ export interface PluginDeclaration {
27
+ readonly id: string;
28
+ readonly version?: string;
29
+ readonly scope: RuntimeScope;
30
+ readonly provides: readonly ServiceKey<unknown>[];
31
+ readonly requires: readonly ServiceKey<unknown>[];
32
+ readonly optional: readonly ServiceKey<unknown>[];
33
+ readonly overrides: readonly ServiceOverride[];
34
+ }
35
+
36
+ export interface ParentServiceInfo {
37
+ readonly pluginId: string;
38
+ readonly keyScope: RuntimeScope;
39
+ readonly providerScope: RuntimeScope;
40
+ }
41
+
42
+ export interface ActivationEdge {
43
+ readonly from: string;
44
+ readonly to: string;
45
+ readonly label: string;
46
+ }
47
+
48
+ export interface ActivationPlan {
49
+ readonly order: readonly string[];
50
+ readonly edges: readonly ActivationEdge[];
51
+ }
52
+
53
+ const RESOLVABLE_SCOPES: Record<RuntimeScope, readonly RuntimeScope[]> = {
54
+ process: ['process'],
55
+ agent: ['agent', 'process'],
56
+ run: ['run', 'agent', 'process'],
57
+ };
58
+
59
+ const VALID_KEY_SCOPES: ReadonlySet<string> = new Set(['process', 'agent', 'run']);
60
+
61
+ interface ProviderEntry {
62
+ readonly declaration: PluginDeclaration;
63
+ readonly key: ServiceKey<unknown>;
64
+ readonly override: ServiceOverride | undefined;
65
+ }
66
+
67
+ function compareIds(a: string, b: string): number {
68
+ if (a < b) return -1;
69
+ if (a > b) return 1;
70
+ return 0;
71
+ }
72
+
73
+ function validatePluginShape(plugin: unknown, scopeKind: RuntimeScope): asserts plugin is CapekPlugin<unknown> {
74
+ if (typeof plugin !== 'object' || plugin === null) {
75
+ throw new MalformedPluginError('plugin entries must be objects');
76
+ }
77
+ const candidate = plugin as Partial<CapekPlugin<unknown>>;
78
+ if (typeof candidate.id !== 'string' || candidate.id.length === 0) {
79
+ throw new MalformedPluginError('plugin id must be a non-empty string');
80
+ }
81
+ if (typeof candidate.setup !== 'function') {
82
+ throw new MalformedPluginError(`plugin '${candidate.id}' must provide a setup function`);
83
+ }
84
+ if (candidate.scope !== scopeKind) {
85
+ throw new ScopeValidationError(
86
+ `plugin '${candidate.id}' is declared for the '${String(candidate.scope)}' scope but is being installed into a '${scopeKind}' scope`,
87
+ );
88
+ }
89
+ }
90
+
91
+ function validateKeyShape(
92
+ pluginId: string,
93
+ field: string,
94
+ key: unknown,
95
+ ): asserts key is ServiceKey<unknown> {
96
+ if (typeof key !== 'object' || key === null) {
97
+ throw new MalformedPluginError(`plugin '${pluginId}' ${field} contains a non-object service key`);
98
+ }
99
+ const candidate = key as Partial<ServiceKey<unknown>>;
100
+ if (typeof candidate.id !== 'string' || candidate.id.length === 0) {
101
+ throw new MalformedPluginError(
102
+ `plugin '${pluginId}' ${field} contains a service key with a non-string or empty id`,
103
+ );
104
+ }
105
+ if (!VALID_KEY_SCOPES.has(candidate.scope as string)) {
106
+ throw new MalformedPluginError(
107
+ `plugin '${pluginId}' ${field} contains service key '${candidate.id}' with invalid scope '${String(candidate.scope)}'`,
108
+ );
109
+ }
110
+ }
111
+
112
+ function validateKeyList(
113
+ pluginId: string,
114
+ field: 'provides' | 'requires' | 'optional',
115
+ value: unknown,
116
+ ): readonly ServiceKey<unknown>[] {
117
+ if (value === undefined || value === null) return [];
118
+ if (!Array.isArray(value)) {
119
+ throw new MalformedPluginError(`plugin '${pluginId}' ${field} must be an array of service keys`);
120
+ }
121
+ for (const key of value) {
122
+ validateKeyShape(pluginId, field, key);
123
+ }
124
+ return value;
125
+ }
126
+
127
+ function validateOverrideList(pluginId: string, value: unknown): readonly ServiceOverride[] {
128
+ if (value === undefined || value === null) return [];
129
+ if (!Array.isArray(value)) {
130
+ throw new MalformedPluginError(`plugin '${pluginId}' overrides must be an array`);
131
+ }
132
+ const seenKeyIds = new Set<string>();
133
+ for (const entry of value) {
134
+ if (typeof entry !== 'object' || entry === null) {
135
+ throw new MalformedPluginError(`plugin '${pluginId}' overrides contains a non-object entry`);
136
+ }
137
+ const candidate = entry as { key?: unknown; replacedProvider?: unknown };
138
+ validateKeyShape(pluginId, 'overrides', candidate.key);
139
+ if (typeof candidate.replacedProvider !== 'string' || candidate.replacedProvider.length === 0) {
140
+ throw new MalformedPluginError(
141
+ `plugin '${pluginId}' override for service '${(candidate.key as ServiceKey<unknown>).id}' must name a non-empty replacedProvider`,
142
+ );
143
+ }
144
+ const keyId = (candidate.key as ServiceKey<unknown>).id;
145
+ if (seenKeyIds.has(keyId)) {
146
+ throw new MalformedPluginError(
147
+ `plugin '${pluginId}' declares more than one override for service '${keyId}'`,
148
+ );
149
+ }
150
+ seenKeyIds.add(keyId);
151
+ }
152
+ return value as readonly ServiceOverride[];
153
+ }
154
+
155
+ function validateProvidedKey(
156
+ plugin: CapekPlugin<unknown>,
157
+ key: ServiceKey<unknown>,
158
+ parentServices: ReadonlyMap<string, ParentServiceInfo>,
159
+ ): void {
160
+ if (key.scope !== plugin.scope) {
161
+ throw new ScopeValidationError(
162
+ `plugin '${plugin.id}' (${plugin.scope}) cannot provide service '${key.id}' with scope '${key.scope}'`,
163
+ );
164
+ }
165
+ const parentProvider = parentServices.get(key.id);
166
+ if (parentProvider !== undefined) {
167
+ throw new ServiceCollisionError(
168
+ `plugin '${plugin.id}' cannot provide service '${key.id}': already provided by plugin '${parentProvider.pluginId}' in the '${parentProvider.providerScope}' scope; child scopes do not replace parent services`,
169
+ );
170
+ }
171
+ }
172
+
173
+ function validateDependencyKey(
174
+ plugin: CapekPlugin<unknown>,
175
+ key: ServiceKey<unknown>,
176
+ kind: 'require' | 'optional',
177
+ ): void {
178
+ if (!RESOLVABLE_SCOPES[plugin.scope].includes(key.scope)) {
179
+ throw new ScopeValidationError(
180
+ `plugin '${plugin.id}' (${plugin.scope}) cannot ${kind} service '${key.id}' with scope '${key.scope}': services must be resolvable from the current or a parent scope`,
181
+ );
182
+ }
183
+ }
184
+
185
+ function validateOverride(
186
+ declaration: PluginDeclaration,
187
+ override: ServiceOverride,
188
+ declarationsById: ReadonlyMap<string, PluginDeclaration>,
189
+ ): void {
190
+ const keyId = override.key.id;
191
+ if (override.key.scope !== declaration.scope) {
192
+ throw new ScopeValidationError(
193
+ `plugin '${declaration.id}' (${declaration.scope}) cannot override service '${keyId}' with scope '${override.key.scope}': an override must target a service of its own scope`,
194
+ );
195
+ }
196
+ if (!declaration.provides.some((key) => key.id === keyId)) {
197
+ throw new InvalidOverrideError(
198
+ `plugin '${declaration.id}' declares an override for service '${keyId}' but does not declare it in provides`,
199
+ );
200
+ }
201
+ if (override.replacedProvider === declaration.id) {
202
+ throw new InvalidOverrideError(
203
+ `plugin '${declaration.id}' overrides service '${keyId}' naming itself as the replaced provider`,
204
+ );
205
+ }
206
+ const replaced = declarationsById.get(override.replacedProvider);
207
+ if (replaced === undefined) {
208
+ throw new InvalidOverrideError(
209
+ `plugin '${declaration.id}' overrides service '${keyId}' naming plugin '${override.replacedProvider}', which is not part of this composition`,
210
+ );
211
+ }
212
+ if (!replaced.provides.some((key) => key.id === keyId)) {
213
+ throw new InvalidOverrideError(
214
+ `plugin '${declaration.id}' overrides service '${keyId}' naming plugin '${override.replacedProvider}', which does not provide that service`,
215
+ );
216
+ }
217
+ }
218
+
219
+ function buildProviderChains(
220
+ declarations: readonly PluginDeclaration[],
221
+ ): Map<string, string> {
222
+ const providersByKey = new Map<string, ProviderEntry[]>();
223
+ for (const declaration of declarations) {
224
+ for (const key of declaration.provides) {
225
+ const entries = providersByKey.get(key.id) ?? [];
226
+ const override = declaration.overrides.find((entry) => entry.key.id === key.id);
227
+ entries.push({ declaration, key, override });
228
+ providersByKey.set(key.id, entries);
229
+ }
230
+ }
231
+
232
+ const effectiveProviders = new Map<string, string>();
233
+ for (const [keyId, providers] of providersByKey) {
234
+ const bases = providers.filter((provider) => provider.override === undefined);
235
+ if (bases.length === 0) {
236
+ const overriders = providers.map((provider) => `'${provider.declaration.id}'`).join(', ');
237
+ throw new InvalidOverrideError(
238
+ `service '${keyId}' has no base provider; overrides declared by plugins ${overriders} cannot establish the service`,
239
+ );
240
+ }
241
+ if (bases.length > 1) {
242
+ const names = bases.map((provider) => `'${provider.declaration.id}'`).join(', ');
243
+ throw new DuplicateProviderError(
244
+ `service '${keyId}' is provided by multiple plugins: ${names}; a replacement requires an explicit override naming the provider being replaced`,
245
+ );
246
+ }
247
+
248
+ let head = bases[0];
249
+ const placed = new Set<string>([head.declaration.id]);
250
+ while (placed.size < providers.length) {
251
+ const candidates = providers.filter(
252
+ (provider) => provider.override !== undefined
253
+ && provider.override.replacedProvider === head.declaration.id
254
+ && !placed.has(provider.declaration.id),
255
+ );
256
+ if (candidates.length > 1) {
257
+ const names = candidates.map((candidate) => `'${candidate.declaration.id}'`).join(', ');
258
+ throw new InvalidOverrideError(
259
+ `service '${keyId}': plugins ${names} both override plugin '${head.declaration.id}'`,
260
+ );
261
+ }
262
+ if (candidates.length === 0) {
263
+ const unplaced = providers
264
+ .filter((provider) => !placed.has(provider.declaration.id))
265
+ .map((provider) => `'${provider.declaration.id}'`).join(', ');
266
+ throw new InvalidOverrideError(
267
+ `service '${keyId}': override chain is broken; plugin(s) ${unplaced} name a replaced provider that is not the effective provider '${head.declaration.id}'`,
268
+ );
269
+ }
270
+ head = candidates[0];
271
+ placed.add(head.declaration.id);
272
+ }
273
+ effectiveProviders.set(keyId, head.declaration.id);
274
+ }
275
+ return effectiveProviders;
276
+ }
277
+
278
+ function findCyclePath(
279
+ remaining: readonly string[],
280
+ adjacency: ReadonlyMap<string, readonly ActivationEdge[]>,
281
+ ): { nodes: string[]; labels: string[] } {
282
+ const remainingSet = new Set(remaining);
283
+ const visited = new Set<string>();
284
+ const stack: string[] = [];
285
+
286
+ function walk(node: string): { nodes: string[]; labels: string[] } | null {
287
+ if (visited.has(node)) {
288
+ const start = stack.indexOf(node);
289
+ if (start === -1) return null;
290
+ const cycleNodes = [...stack.slice(start), node];
291
+ const labels = cycleNodes.slice(0, -1).map((from, index) => {
292
+ const to = cycleNodes[index + 1];
293
+ return (adjacency.get(from) ?? []).find((edge) => edge.to === to)?.label ?? '';
294
+ });
295
+ return { nodes: cycleNodes, labels };
296
+ }
297
+ visited.add(node);
298
+ stack.push(node);
299
+ for (const edge of adjacency.get(node) ?? []) {
300
+ if (!remainingSet.has(edge.to)) continue;
301
+ const found = walk(edge.to);
302
+ if (found !== null) return found;
303
+ }
304
+ stack.pop();
305
+ return null;
306
+ }
307
+
308
+ for (const node of remaining) {
309
+ const found = walk(node);
310
+ if (found !== null) return found;
311
+ }
312
+ return { nodes: [], labels: [] };
313
+ }
314
+
315
+ function topologicalOrder(
316
+ declarations: readonly PluginDeclaration[],
317
+ edges: readonly ActivationEdge[],
318
+ ): string[] {
319
+ const nodeIds = declarations.map((declaration) => declaration.id);
320
+ const adjacency = new Map<string, ActivationEdge[]>();
321
+ const inDegree = new Map<string, number>();
322
+ for (const id of nodeIds) {
323
+ adjacency.set(id, []);
324
+ inDegree.set(id, 0);
325
+ }
326
+ for (const edge of edges) {
327
+ adjacency.get(edge.from)?.push(edge);
328
+ inDegree.set(edge.to, (inDegree.get(edge.to) ?? 0) + 1);
329
+ }
330
+
331
+ const available = nodeIds.filter((id) => inDegree.get(id) === 0).sort(compareIds);
332
+ const order: string[] = [];
333
+ while (available.length > 0) {
334
+ const node = available.shift() as string;
335
+ order.push(node);
336
+ for (const edge of adjacency.get(node) ?? []) {
337
+ const nextDegree = (inDegree.get(edge.to) ?? 0) - 1;
338
+ inDegree.set(edge.to, nextDegree);
339
+ if (nextDegree === 0) {
340
+ available.push(edge.to);
341
+ available.sort(compareIds);
342
+ }
343
+ }
344
+ }
345
+
346
+ if (order.length < nodeIds.length) {
347
+ const remaining = nodeIds.filter((id) => !order.includes(id));
348
+ const cycle = findCyclePath(remaining, adjacency);
349
+ const parts = cycle.nodes.map((id, index) => {
350
+ const label = cycle.labels[index] ?? '';
351
+ return label.length > 0 ? `plugin '${id}' (${label})` : `plugin '${id}'`;
352
+ });
353
+ throw new DependencyCycleError(`dependency cycle detected: ${parts.join(' -> ')}`);
354
+ }
355
+ return order;
356
+ }
357
+
358
+ /** A dependency key and its provider must carry the same ServiceKey scope.
359
+ * Resolving through a mismatched scope would hand the wrong contract type to
360
+ * the consumer, so the composition is rejected before setup. */
361
+ function assertMatchingProviderScope(
362
+ declaration: PluginDeclaration,
363
+ key: ServiceKey<unknown>,
364
+ providerId: string,
365
+ declarationsById: ReadonlyMap<string, PluginDeclaration>,
366
+ kind: 'requires' | 'optional',
367
+ ): void {
368
+ const provider = declarationsById.get(providerId);
369
+ const providerKey = provider?.provides.find((candidate) => candidate.id === key.id);
370
+ if (providerKey === undefined || providerKey.scope !== key.scope) {
371
+ throw new ScopeValidationError(
372
+ `plugin '${declaration.id}' ${kind === 'requires' ? 'requires' : 'optionally requires'} service '${key.id}' declared with scope '${key.scope}' but provider plugin '${providerId}' provides it with scope '${String(providerKey?.scope)}'; conflicting ServiceKey scopes cannot satisfy the dependency`,
373
+ );
374
+ }
375
+ }
376
+
377
+ function assertMatchingParentScope(
378
+ declaration: PluginDeclaration,
379
+ key: ServiceKey<unknown>,
380
+ parentInfo: ParentServiceInfo,
381
+ kind: 'requires' | 'optional',
382
+ ): void {
383
+ if (parentInfo.keyScope !== key.scope) {
384
+ throw new ScopeValidationError(
385
+ `plugin '${declaration.id}' ${kind === 'requires' ? 'requires' : 'optionally requires'} service '${key.id}' declared with scope '${key.scope}' but parent scope plugin '${parentInfo.pluginId}' provides it with scope '${parentInfo.keyScope}'; conflicting ServiceKey scopes cannot satisfy the dependency`,
386
+ );
387
+ }
388
+ }
389
+
390
+ export function planActivation(
391
+ scopeKind: RuntimeScope,
392
+ plugins: readonly CapekPlugin<unknown>[],
393
+ parentServices: ReadonlyMap<string, ParentServiceInfo>,
394
+ ): ActivationPlan {
395
+ if (!Array.isArray(plugins)) {
396
+ throw new MalformedPluginError('plugins must be an array');
397
+ }
398
+ for (const plugin of plugins) {
399
+ validatePluginShape(plugin, scopeKind);
400
+ }
401
+
402
+ const declarationsById = new Map<string, PluginDeclaration>();
403
+ for (const plugin of plugins) {
404
+ if (declarationsById.has(plugin.id)) {
405
+ throw new DuplicatePluginError(
406
+ `duplicate plugin id '${plugin.id}' in one composition`,
407
+ );
408
+ }
409
+ const provides = validateKeyList(plugin.id, 'provides', plugin.provides);
410
+ const requires = validateKeyList(plugin.id, 'requires', plugin.requires);
411
+ const optional = validateKeyList(plugin.id, 'optional', plugin.optional);
412
+ const overrides = validateOverrideList(plugin.id, plugin.overrides);
413
+ const providedIds = new Set<string>();
414
+ for (const key of provides) {
415
+ if (providedIds.has(key.id)) {
416
+ throw new MalformedPluginError(
417
+ `plugin '${plugin.id}' declares service '${key.id}' more than once`,
418
+ );
419
+ }
420
+ providedIds.add(key.id);
421
+ validateProvidedKey(plugin, key, parentServices);
422
+ }
423
+ for (const key of requires) {
424
+ validateDependencyKey(plugin, key, 'require');
425
+ }
426
+ for (const key of optional) {
427
+ validateDependencyKey(plugin, key, 'optional');
428
+ }
429
+ declarationsById.set(plugin.id, {
430
+ id: plugin.id,
431
+ version: plugin.version,
432
+ scope: plugin.scope,
433
+ provides,
434
+ requires,
435
+ optional,
436
+ overrides,
437
+ });
438
+ }
439
+
440
+ for (const declaration of declarationsById.values()) {
441
+ for (const override of declaration.overrides) {
442
+ validateOverride(declaration, override, declarationsById);
443
+ }
444
+ }
445
+
446
+ const declarations = [...declarationsById.values()];
447
+ const effectiveProviders = buildProviderChains(declarations);
448
+
449
+ const edges: ActivationEdge[] = [];
450
+ for (const declaration of declarations) {
451
+ for (const key of declaration.requires) {
452
+ const providerId = effectiveProviders.get(key.id);
453
+ if (providerId !== undefined) {
454
+ assertMatchingProviderScope(declaration, key, providerId, declarationsById, 'requires');
455
+ edges.push({
456
+ from: providerId,
457
+ to: declaration.id,
458
+ label: `requires service '${key.id}'`,
459
+ });
460
+ } else if (parentServices.has(key.id)) {
461
+ const parentInfo = parentServices.get(key.id) as ParentServiceInfo;
462
+ assertMatchingParentScope(declaration, key, parentInfo, 'requires');
463
+ } else {
464
+ throw new MissingDependencyError(
465
+ `plugin '${declaration.id}' requires service '${key.id}' (scope '${key.scope}') which is not provided in this composition or any parent scope`,
466
+ );
467
+ }
468
+ }
469
+ for (const key of declaration.optional) {
470
+ const providerId = effectiveProviders.get(key.id);
471
+ if (providerId !== undefined) {
472
+ assertMatchingProviderScope(declaration, key, providerId, declarationsById, 'optional');
473
+ edges.push({
474
+ from: providerId,
475
+ to: declaration.id,
476
+ label: `optionally requires service '${key.id}'`,
477
+ });
478
+ } else if (parentServices.has(key.id)) {
479
+ const parentInfo = parentServices.get(key.id) as ParentServiceInfo;
480
+ assertMatchingParentScope(declaration, key, parentInfo, 'optional');
481
+ }
482
+ }
483
+ for (const override of declaration.overrides) {
484
+ edges.push({
485
+ from: override.replacedProvider,
486
+ to: declaration.id,
487
+ label: `overrides service '${override.key.id}'`,
488
+ });
489
+ }
490
+ }
491
+
492
+ return { order: topologicalOrder(declarations, edges), edges };
493
+ }