@pulse-compute/wasm-contracts 0.0.0 → 1.0.0-beta.1

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 (96) hide show
  1. package/README.md +34 -2
  2. package/package.json +123 -6
  3. package/src/assets/contracts.js +296 -0
  4. package/src/config/provider-resolution.js +54 -0
  5. package/src/config/runtime-provider-kv.js +73 -0
  6. package/src/config/symbolic-refs.js +192 -0
  7. package/src/crypto/contracts.js +637 -0
  8. package/src/definitions/config-schema.js +30 -0
  9. package/src/definitions/handler-roles.js +66 -0
  10. package/src/definitions/path-grammar.js +11 -0
  11. package/src/definitions/router-api.js +146 -0
  12. package/src/diagnostics/codes.js +42 -0
  13. package/src/diagnostics/reporter.js +20 -0
  14. package/src/diagnostics.js +139 -0
  15. package/src/dispatch-table.js +23 -0
  16. package/src/entities/catalog.d.ts +7 -0
  17. package/src/entities/catalog.js +87 -0
  18. package/src/entities/contracts.d.ts +95 -0
  19. package/src/entities/contracts.js +122 -0
  20. package/src/entities/json-rpc.d.ts +27 -0
  21. package/src/entities/json-rpc.js +69 -0
  22. package/src/entities/runtime.d.ts +67 -0
  23. package/src/entities/runtime.js +288 -0
  24. package/src/events/contracts.js +456 -0
  25. package/src/execution-plan.js +42 -0
  26. package/src/grip/contracts.js +106 -0
  27. package/src/handler/backend-json-request-body.js +58 -0
  28. package/src/handler/beta-lifecycle-golden.js +97 -0
  29. package/src/handler/canonical-native-plan.js +143 -0
  30. package/src/handler/canonical-native-runtime.js +168 -0
  31. package/src/handler/canonical-runtime.js +13 -0
  32. package/src/handler/context.js +131 -0
  33. package/src/handler/ctx-contract.js +71 -0
  34. package/src/handler/doctor-readiness-aggregator.js +67 -0
  35. package/src/handler/effects.js +446 -0
  36. package/src/handler/io-lifecycle.js +134 -0
  37. package/src/handler/lifecycle-authoring-alignment.js +52 -0
  38. package/src/handler/lifecycle-readiness-aggregate.js +93 -0
  39. package/src/handler/reference-node-lifecycle-golden.js +73 -0
  40. package/src/handler/request-json-body.js +147 -0
  41. package/src/handler/runtime-api.js +471 -0
  42. package/src/handler/schema-decode-result.js +135 -0
  43. package/src/handler/schema-response-codec.js +141 -0
  44. package/src/handler/surface-contract.js +594 -0
  45. package/src/handler/types-contract.js +51 -0
  46. package/src/handler-table.js +28 -0
  47. package/src/host/backend-capabilities.js +24 -0
  48. package/src/host/capabilities.js +273 -0
  49. package/src/host/channel-broadcaster.js +60 -0
  50. package/src/host/json-body.js +80 -0
  51. package/src/host/request-result-headers.js +38 -0
  52. package/src/host/streaming-passthrough.js +90 -0
  53. package/src/host/wasm-host-abi.js +154 -0
  54. package/src/index.js +75 -0
  55. package/src/jwt/contracts.js +427 -0
  56. package/src/library/api-surface.js +99 -0
  57. package/src/library/contracts.js +273 -0
  58. package/src/library/manifest.js +317 -0
  59. package/src/library/second-package-owned-lowering.js +39 -0
  60. package/src/library/sidecars.js +147 -0
  61. package/src/logging.js +89 -0
  62. package/src/package/package-contract.js +975 -0
  63. package/src/package/package-contract.schema.json +303 -0
  64. package/src/package/runtime-pulse-live.js +100 -0
  65. package/src/path-table.js +12 -0
  66. package/src/path.js +275 -0
  67. package/src/project/config-factory.js +48 -0
  68. package/src/project/config-plan.js +409 -0
  69. package/src/project/config-runtime.js +46 -0
  70. package/src/project/javascript-application.js +212 -0
  71. package/src/project/javascript-core-capabilities.js +44 -0
  72. package/src/project/native-optimization.js +9 -0
  73. package/src/project/package-ownership.js +242 -0
  74. package/src/project/package-product-projection.js +171 -0
  75. package/src/project/reachable-graph-identity.js +219 -0
  76. package/src/project/reachable-graph-projections.js +279 -0
  77. package/src/project/reachable-graph-resolver.js +303 -0
  78. package/src/project/reachable-graph-v2.js +430 -0
  79. package/src/project/reachable-graph-v2.schema.json +285 -0
  80. package/src/project/reachable-graph.js +199 -0
  81. package/src/project/reachable-graph.schema.json +222 -0
  82. package/src/project/target-support-evidence.js +559 -0
  83. package/src/provider/canonical-provider.js +235 -0
  84. package/src/provider/final-wasm-policy.js +87 -0
  85. package/src/provider/toolchain.js +771 -0
  86. package/src/route-plan.js +43 -0
  87. package/src/schema-json/compile.js +63 -0
  88. package/src/schema-json/contracts.js +10 -0
  89. package/src/schema-json/generic-parser.js +62 -0
  90. package/src/schema-json/registry.js +359 -0
  91. package/src/schema-json/semantic-trace.js +241 -0
  92. package/src/schema-json/v1.js +65 -0
  93. package/src/schema-json/v2.js +110 -0
  94. package/src/sha256.js +104 -0
  95. package/src/spine.js +381 -0
  96. package/src/stable-id.js +142 -0
@@ -0,0 +1,48 @@
1
+ 'use strict';
2
+
3
+ const CONFIG_FACTORY_CONTRACT_VERSION = 'pulse.config-factory.v1';
4
+ const CONFIG_SCOPE_CONTRACT_VERSION = 'pulse.config-scope.v1';
5
+ const CONFIG_FACTORY_BRAND = Symbol.for('pulse.config-factory.v1');
6
+ const CONFIG_FACTORY_METADATA = Symbol.for('pulse.config-factory.metadata.v1');
7
+
8
+ function isConfigFactory(value) {
9
+ return typeof value === 'function' && value[CONFIG_FACTORY_BRAND] === CONFIG_FACTORY_CONTRACT_VERSION;
10
+ }
11
+
12
+ function defineConfig(factory) {
13
+ if (typeof factory !== 'function') {
14
+ throw new TypeError('defineConfig requires one synchronous configuration factory.');
15
+ }
16
+ if (factory.constructor && factory.constructor.name === 'AsyncFunction') {
17
+ throw new TypeError('defineConfig does not accept async configuration factories.');
18
+ }
19
+ if (!Object.prototype.hasOwnProperty.call(factory, CONFIG_FACTORY_BRAND)) {
20
+ Object.defineProperties(factory, {
21
+ [CONFIG_FACTORY_BRAND]: {
22
+ value: CONFIG_FACTORY_CONTRACT_VERSION,
23
+ enumerable: false,
24
+ configurable: false,
25
+ writable: false
26
+ },
27
+ [CONFIG_FACTORY_METADATA]: {
28
+ value: Object.freeze({
29
+ version: CONFIG_FACTORY_CONTRACT_VERSION,
30
+ scopeVersion: CONFIG_SCOPE_CONTRACT_VERSION
31
+ }),
32
+ enumerable: false,
33
+ configurable: false,
34
+ writable: false
35
+ }
36
+ });
37
+ }
38
+ return Object.freeze(factory);
39
+ }
40
+
41
+ module.exports = Object.freeze({
42
+ CONFIG_FACTORY_CONTRACT_VERSION,
43
+ CONFIG_SCOPE_CONTRACT_VERSION,
44
+ CONFIG_FACTORY_BRAND,
45
+ CONFIG_FACTORY_METADATA,
46
+ isConfigFactory,
47
+ defineConfig
48
+ });
@@ -0,0 +1,409 @@
1
+ 'use strict';
2
+
3
+ const path = require('node:path');
4
+ const { stableStringify, sha256Hex } = require('../stable-id.js');
5
+ const {
6
+ DEFAULT_REPORTING_LEVEL,
7
+ normalizeReportingLevel,
8
+ resolveReportingLevel
9
+ } = require('../logging.js');
10
+ const {
11
+ CONFIG_FACTORY_CONTRACT_VERSION,
12
+ CONFIG_SCOPE_CONTRACT_VERSION,
13
+ CONFIG_FACTORY_BRAND,
14
+ CONFIG_FACTORY_METADATA,
15
+ isConfigFactory
16
+ } = require('./config-factory.js');
17
+ const {
18
+ normalizeCryptoConfiguration,
19
+ emptyCryptoConfiguration
20
+ } = require('../crypto/contracts.js');
21
+
22
+ const PROJECT_DECLARATION_VERSION = 'pulse.project-declaration.v2';
23
+ const PROFILE_PLAN_VERSION = 'pulse.profile-plan.v2';
24
+
25
+ const PROFILE_SELECTION_SOURCES = Object.freeze(['cli', 'environment', 'default']);
26
+ const EXECUTION_TARGETS = Object.freeze(['native', 'javascript']);
27
+ const PULSE_RESERVED_KEYS = Object.freeze(['entry', 'schema', 'tests', 'defaultProfile', 'strict', 'reporting', 'crypto']);
28
+ const PROFILE_CORE_KEYS = Object.freeze(['host', 'target', 'reporting', 'crypto']);
29
+ const DEFAULT_PROJECT_POINTERS = Object.freeze({
30
+ entry: 'src/index.ts',
31
+ schema: null,
32
+ tests: null
33
+ });
34
+
35
+ function contractError(code, message, details = {}) {
36
+ const error = new TypeError(message);
37
+ error.code = code;
38
+ error.details = Object.freeze({ ...details });
39
+ return error;
40
+ }
41
+
42
+ function isPlainObject(value) {
43
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
44
+ const proto = Object.getPrototypeOf(value);
45
+ return proto === Object.prototype || proto === null;
46
+ }
47
+
48
+ function deepFreeze(value) {
49
+ if (Array.isArray(value)) {
50
+ for (const child of value) deepFreeze(child);
51
+ return Object.freeze(value);
52
+ }
53
+ if (isPlainObject(value)) {
54
+ for (const child of Object.values(value)) deepFreeze(child);
55
+ return Object.freeze(value);
56
+ }
57
+ return value;
58
+ }
59
+
60
+ function nonEmptyString(value, field) {
61
+ if (typeof value !== 'string' || value.trim().length === 0) {
62
+ throw contractError('PULSE_PROJECT_PLAN_STRING_REQUIRED', `${field} must be a non-empty string.`, { field, value });
63
+ }
64
+ return value.trim();
65
+ }
66
+
67
+ function normalizeWorkspacePointer(value, field, options = {}) {
68
+ if (value === undefined || value === null || value === '') {
69
+ if (options.required) throw contractError('PULSE_PROJECT_POINTER_REQUIRED', `${field} is required.`, { field });
70
+ return null;
71
+ }
72
+ const raw = nonEmptyString(value, field).replace(/\\/g, '/');
73
+ if (raw.startsWith('/') || /^[a-zA-Z]:\//.test(raw)) {
74
+ throw contractError('PULSE_PROJECT_POINTER_ABSOLUTE', `${field} must be workspace-relative.`, { field, value: raw });
75
+ }
76
+ const normalized = path.posix.normalize(raw).replace(/^\.\//, '');
77
+ if (normalized === '..' || normalized.startsWith('../')) {
78
+ throw contractError('PULSE_PROJECT_POINTER_ESCAPE', `${field} must remain inside the Pulse workspace.`, { field, value: raw });
79
+ }
80
+ if (normalized === '.' || normalized.length === 0) {
81
+ throw contractError('PULSE_PROJECT_POINTER_INVALID', `${field} must name a workspace-relative file or directory.`, { field, value: raw });
82
+ }
83
+ return normalized;
84
+ }
85
+
86
+ function symbolicKind(value) {
87
+ if (!isPlainObject(value)) return null;
88
+ const hasConfig = Object.prototype.hasOwnProperty.call(value, '$config');
89
+ const hasSecret = Object.prototype.hasOwnProperty.call(value, '$secret');
90
+ if (hasConfig && hasSecret) return 'mixed';
91
+ if (hasConfig) return 'config';
92
+ if (hasSecret) return 'secret';
93
+ return null;
94
+ }
95
+
96
+ function normalizeSymbolicRef(value, location) {
97
+ const kind = symbolicKind(value);
98
+ if (!kind) return null;
99
+ if (kind === 'mixed') {
100
+ throw contractError('PULSE_CONFIG_SCOPE_REF_MIXED', `${location} cannot be both a config and secret reference.`, { location });
101
+ }
102
+ const allowedKey = kind === 'config' ? '$config' : '$secret';
103
+ const keys = Object.keys(value);
104
+ if (keys.length !== 1 || keys[0] !== allowedKey) {
105
+ throw contractError('PULSE_CONFIG_SCOPE_REF_SHAPE', `${location} must contain exactly one ${allowedKey} property.`, { location, keys });
106
+ }
107
+ const name = nonEmptyString(value[allowedKey], `${location}.${allowedKey}`);
108
+ return Object.freeze({ [allowedKey]: name });
109
+ }
110
+
111
+ function normalizeStaticValue(value, location, refs) {
112
+ const symbolic = normalizeSymbolicRef(value, location);
113
+ if (symbolic) {
114
+ const kind = symbolicKind(symbolic);
115
+ const name = symbolic[kind === 'config' ? '$config' : '$secret'];
116
+ refs.push(Object.freeze({ kind, name, path: location }));
117
+ return symbolic;
118
+ }
119
+ if (value === null || typeof value === 'string' || typeof value === 'boolean') return value;
120
+ if (typeof value === 'number') {
121
+ if (!Number.isFinite(value)) throw contractError('PULSE_PROJECT_PLAN_NUMBER_INVALID', `${location} must be a finite number.`, { location, value });
122
+ return value;
123
+ }
124
+ if (Array.isArray(value)) {
125
+ return Object.freeze(value.map((entry, index) => normalizeStaticValue(entry, `${location}[${index}]`, refs)));
126
+ }
127
+ if (isPlainObject(value)) {
128
+ const out = {};
129
+ for (const key of Object.keys(value).sort()) {
130
+ const child = value[key];
131
+ if (child === undefined) {
132
+ throw contractError('PULSE_PROJECT_PLAN_UNDEFINED', `${location}.${key} cannot be undefined.`, { location: `${location}.${key}` });
133
+ }
134
+ out[key] = normalizeStaticValue(child, `${location}.${key}`, refs);
135
+ }
136
+ return deepFreeze(out);
137
+ }
138
+ throw contractError('PULSE_PROJECT_PLAN_VALUE_UNSUPPORTED', `${location} contains a non-static value.`, {
139
+ location,
140
+ type: typeof value
141
+ });
142
+ }
143
+
144
+ function assertNoMixedBindingNames(refs) {
145
+ const kindsByName = new Map();
146
+ for (const ref of refs) {
147
+ const kinds = kindsByName.get(ref.name) || new Set();
148
+ kinds.add(ref.kind);
149
+ kindsByName.set(ref.name, kinds);
150
+ }
151
+ for (const [name, kinds] of kindsByName) {
152
+ if (kinds.size > 1) {
153
+ throw contractError('PULSE_CONFIG_SCOPE_NAME_CLASS_CONFLICT', `Logical binding ${name} cannot be both config and secret in one project.`, {
154
+ name,
155
+ kinds: Array.from(kinds).sort()
156
+ });
157
+ }
158
+ }
159
+ }
160
+
161
+ function bindingsForRefs(refs) {
162
+ return deepFreeze({
163
+ config: Array.from(new Set(refs.filter((ref) => ref.kind === 'config').map((ref) => ref.name))).sort(),
164
+ secret: Array.from(new Set(refs.filter((ref) => ref.kind === 'secret').map((ref) => ref.name))).sort()
165
+ });
166
+ }
167
+
168
+ function normalizePulseObject(value = {}) {
169
+ if (!isPlainObject(value)) throw contractError('PULSE_PROJECT_PULSE_OBJECT_REQUIRED', 'The reserved pulse key must contain a plain object.');
170
+ for (const key of Object.keys(value)) {
171
+ if (!PULSE_RESERVED_KEYS.includes(key)) {
172
+ throw contractError('PULSE_PROJECT_PULSE_KEY_UNSUPPORTED', `Unsupported reserved pulse key: ${key}`, { key });
173
+ }
174
+ }
175
+ const strict = value.strict === undefined ? true : value.strict;
176
+ if (typeof strict !== 'boolean') throw contractError('PULSE_PROJECT_STRICT_BOOLEAN_REQUIRED', 'pulse.strict must be a boolean.', { value: strict });
177
+ const defaultProfile = value.defaultProfile === undefined || value.defaultProfile === null
178
+ ? null
179
+ : nonEmptyString(value.defaultProfile, 'pulse.defaultProfile');
180
+ const reporting = normalizeReportingLevel(value.reporting ?? DEFAULT_REPORTING_LEVEL, 'pulse.reporting');
181
+ const crypto = Object.prototype.hasOwnProperty.call(value, 'crypto')
182
+ ? normalizeCryptoConfiguration(value.crypto, { location: 'pulse.crypto' })
183
+ : null;
184
+ return deepFreeze({
185
+ entry: normalizeWorkspacePointer(value.entry ?? DEFAULT_PROJECT_POINTERS.entry, 'pulse.entry', { required: true }),
186
+ schema: normalizeWorkspacePointer(value.schema ?? DEFAULT_PROJECT_POINTERS.schema, 'pulse.schema'),
187
+ tests: normalizeWorkspacePointer(value.tests ?? DEFAULT_PROJECT_POINTERS.tests, 'pulse.tests'),
188
+ defaultProfile,
189
+ strict,
190
+ reporting,
191
+ crypto
192
+ });
193
+ }
194
+
195
+ function normalizeProfile(name, value, allRefs) {
196
+ const profileName = nonEmptyString(name, 'profile name');
197
+ if (profileName === 'pulse') throw contractError('PULSE_PROJECT_PROFILE_RESERVED', 'pulse is reserved and cannot be a profile name.');
198
+ if (!isPlainObject(value)) throw contractError('PULSE_PROJECT_PROFILE_OBJECT_REQUIRED', `Profile ${profileName} must be a plain object.`, { profile: profileName });
199
+ const host = nonEmptyString(value.host, `${profileName}.host`);
200
+ const target = nonEmptyString(value.target, `${profileName}.target`);
201
+ if (!EXECUTION_TARGETS.includes(target)) {
202
+ throw contractError('PULSE_PROJECT_TARGET_UNSUPPORTED', `Profile ${profileName} target must be native or javascript.`, { profile: profileName, target });
203
+ }
204
+ const reporting = value.reporting === undefined || value.reporting === null
205
+ ? null
206
+ : normalizeReportingLevel(value.reporting, `${profileName}.reporting`);
207
+ const crypto = Object.prototype.hasOwnProperty.call(value, 'crypto')
208
+ ? normalizeCryptoConfiguration(value.crypto, { location: `${profileName}.crypto` })
209
+ : null;
210
+ const profileRefs = [];
211
+ const fragments = {};
212
+ for (const key of Object.keys(value).sort()) {
213
+ if (PROFILE_CORE_KEYS.includes(key)) continue;
214
+ fragments[key] = normalizeStaticValue(value[key], `${profileName}.${key}`, profileRefs);
215
+ }
216
+ allRefs.push(...profileRefs);
217
+ return deepFreeze({
218
+ name: profileName,
219
+ host,
220
+ target,
221
+ reporting,
222
+ crypto,
223
+ fragments,
224
+ bindings: bindingsForRefs(profileRefs)
225
+ });
226
+ }
227
+
228
+ function withStableHash(value, field = 'hash', hashValue = value) {
229
+ const copy = { ...value };
230
+ copy[field] = sha256Hex(stableStringify(hashValue));
231
+ return deepFreeze(copy);
232
+ }
233
+
234
+ function normalizeProjectDeclaration(input, options = {}) {
235
+ if (!isPlainObject(input)) throw contractError('PULSE_PROJECT_DECLARATION_OBJECT_REQUIRED', 'Pulse config factory must return a plain object.');
236
+ const pulse = normalizePulseObject(input.pulse || {});
237
+ const profileNames = Object.keys(input).filter((key) => key !== 'pulse').sort();
238
+ if (profileNames.length === 0) throw contractError('PULSE_PROJECT_PROFILE_REQUIRED', 'Pulse project config must declare at least one profile.');
239
+
240
+ const refs = [];
241
+ const profiles = {};
242
+ for (const name of profileNames) profiles[name] = normalizeProfile(name, input[name], refs);
243
+ assertNoMixedBindingNames(refs);
244
+ if (pulse.defaultProfile !== null && !Object.prototype.hasOwnProperty.call(profiles, pulse.defaultProfile)) {
245
+ throw contractError('PULSE_PROJECT_DEFAULT_PROFILE_UNKNOWN', `pulse.defaultProfile references unknown profile ${pulse.defaultProfile}.`, {
246
+ defaultProfile: pulse.defaultProfile,
247
+ profiles: profileNames
248
+ });
249
+ }
250
+
251
+ const semantic = {
252
+ version: PROJECT_DECLARATION_VERSION,
253
+ configFactoryVersion: CONFIG_FACTORY_CONTRACT_VERSION,
254
+ configScopeVersion: CONFIG_SCOPE_CONTRACT_VERSION,
255
+ pulse,
256
+ profiles,
257
+ bindings: bindingsForRefs(refs)
258
+ };
259
+ return withStableHash({
260
+ ...semantic,
261
+ source: options.source ? String(options.source) : null
262
+ }, 'projectHash', semantic);
263
+ }
264
+
265
+ function resolveProfileSelection(input = {}) {
266
+ const cliProfile = input.cliProfile === undefined || input.cliProfile === null || input.cliProfile === ''
267
+ ? null
268
+ : nonEmptyString(input.cliProfile, 'cli profile');
269
+ const environmentProfile = input.environmentProfile === undefined || input.environmentProfile === null || input.environmentProfile === ''
270
+ ? null
271
+ : nonEmptyString(input.environmentProfile, 'environment profile');
272
+ const defaultProfile = input.defaultProfile === undefined || input.defaultProfile === null || input.defaultProfile === ''
273
+ ? null
274
+ : nonEmptyString(input.defaultProfile, 'default profile');
275
+
276
+ if (cliProfile) return Object.freeze({ name: cliProfile, source: 'cli' });
277
+ if (environmentProfile) return Object.freeze({ name: environmentProfile, source: 'environment' });
278
+ if (defaultProfile) return Object.freeze({ name: defaultProfile, source: 'default' });
279
+ throw contractError('PULSE_PROFILE_SELECTION_REQUIRED', 'Select a profile with --profile, PULSE_PROFILE, or pulse.defaultProfile.');
280
+ }
281
+
282
+ function selectedProfileRefs(profile) {
283
+ const refs = [];
284
+ normalizeStaticValue(profile.fragments, profile.name, refs);
285
+ return refs;
286
+ }
287
+
288
+ function selectProfilePlan(projectDeclaration, selectionInput = {}) {
289
+ const declaration = projectDeclaration && projectDeclaration.version === PROJECT_DECLARATION_VERSION
290
+ ? projectDeclaration
291
+ : normalizeProjectDeclaration(projectDeclaration);
292
+ const selection = selectionInput.name && PROFILE_SELECTION_SOURCES.includes(selectionInput.source)
293
+ ? Object.freeze({ name: nonEmptyString(selectionInput.name, 'selected profile'), source: selectionInput.source })
294
+ : resolveProfileSelection({
295
+ cliProfile: selectionInput.cliProfile,
296
+ environmentProfile: selectionInput.environmentProfile,
297
+ defaultProfile: declaration.pulse.defaultProfile
298
+ });
299
+ const profile = declaration.profiles[selection.name];
300
+ if (!profile) {
301
+ throw contractError('PULSE_PROFILE_UNKNOWN', `Unknown Pulse profile ${selection.name}.`, {
302
+ profile: selection.name,
303
+ profiles: Object.keys(declaration.profiles)
304
+ });
305
+ }
306
+ const refs = selectedProfileRefs(profile);
307
+ assertNoMixedBindingNames(refs);
308
+ const reporting = resolveReportingLevel(profile.reporting, declaration.pulse.reporting);
309
+ const crypto = profile.crypto !== null
310
+ ? deepFreeze({ source: 'profile', declaration: profile.crypto })
311
+ : (declaration.pulse.crypto !== null
312
+ ? deepFreeze({ source: 'pulse', declaration: declaration.pulse.crypto })
313
+ : deepFreeze({ source: 'implicit', declaration: emptyCryptoConfiguration() }));
314
+ return withStableHash({
315
+ version: PROFILE_PLAN_VERSION,
316
+ projectVersion: declaration.version,
317
+ projectHash: declaration.projectHash,
318
+ pulse: declaration.pulse,
319
+ profile: {
320
+ name: profile.name,
321
+ source: selection.source,
322
+ host: profile.host,
323
+ target: profile.target,
324
+ reporting
325
+ },
326
+ crypto,
327
+ fragments: profile.fragments,
328
+ bindings: bindingsForRefs(refs)
329
+ }, 'planHash');
330
+ }
331
+
332
+ function symbolicRef(kind, name) {
333
+ const logicalName = nonEmptyString(name, `scope.${kind}`);
334
+ return Object.freeze({ [kind === 'config' ? '$config' : '$secret']: logicalName });
335
+ }
336
+
337
+ function createConfigScope() {
338
+ return Object.freeze({
339
+ version: CONFIG_SCOPE_CONTRACT_VERSION,
340
+ config(name) { return symbolicRef('config', name); },
341
+ secret(name) { return symbolicRef('secret', name); }
342
+ });
343
+ }
344
+
345
+ function invokeConfigFactory(factory, options = {}) {
346
+ if (!isConfigFactory(factory)) {
347
+ throw contractError('PULSE_CONFIG_FACTORY_REQUIRED', 'Pulse config must export defineConfig((scope) => ({ ... })).');
348
+ }
349
+ const declaration = factory(createConfigScope());
350
+ if (declaration && typeof declaration.then === 'function') {
351
+ throw contractError('PULSE_CONFIG_FACTORY_ASYNC_UNSUPPORTED', 'Pulse config factories must be synchronous.');
352
+ }
353
+ return normalizeProjectDeclaration(declaration, options);
354
+ }
355
+
356
+ function defaultProjectPlanContract() {
357
+ return deepFreeze({
358
+ projectDeclarationVersion: PROJECT_DECLARATION_VERSION,
359
+ profilePlanVersion: PROFILE_PLAN_VERSION,
360
+ configFactoryVersion: CONFIG_FACTORY_CONTRACT_VERSION,
361
+ configScopeVersion: CONFIG_SCOPE_CONTRACT_VERSION,
362
+ profileSelectionPrecedence: PROFILE_SELECTION_SOURCES,
363
+ targets: EXECUTION_TARGETS,
364
+ pulseReservedKeys: PULSE_RESERVED_KEYS,
365
+ profileCoreKeys: PROFILE_CORE_KEYS,
366
+ defaults: {
367
+ pointers: DEFAULT_PROJECT_POINTERS,
368
+ strict: true
369
+ },
370
+ policies: {
371
+ workspaceRelativePointers: true,
372
+ implicitLocalProfile: false,
373
+ implicitFirstProfile: false,
374
+ nodeEnvProfileInference: false,
375
+ commandProfileInference: false,
376
+ selectedProfilePassedToFactory: false,
377
+ resolvedSecretValuesAllowed: false,
378
+ secretLiteralFallbackAllowed: false,
379
+ sameNameConfigAndSecretAllowed: false,
380
+ genericDeepMerge: false,
381
+ cryptoProfileReplacement: true,
382
+ cryptoRuntimeDetection: false,
383
+ cryptoAutomaticFallback: false
384
+ }
385
+ });
386
+ }
387
+
388
+ module.exports = {
389
+ PROJECT_DECLARATION_VERSION,
390
+ PROFILE_PLAN_VERSION,
391
+ CONFIG_FACTORY_CONTRACT_VERSION,
392
+ CONFIG_SCOPE_CONTRACT_VERSION,
393
+ CONFIG_FACTORY_BRAND,
394
+ CONFIG_FACTORY_METADATA,
395
+ PROFILE_SELECTION_SOURCES,
396
+ EXECUTION_TARGETS,
397
+ PULSE_RESERVED_KEYS,
398
+ PROFILE_CORE_KEYS,
399
+ DEFAULT_PROJECT_POINTERS,
400
+ createConfigScope,
401
+ isConfigFactory,
402
+ invokeConfigFactory,
403
+ isPlainObject,
404
+ normalizeWorkspacePointer,
405
+ normalizeProjectDeclaration,
406
+ resolveProfileSelection,
407
+ selectProfilePlan,
408
+ defaultProjectPlanContract
409
+ };
@@ -0,0 +1,46 @@
1
+ 'use strict';
2
+
3
+ const {
4
+ createConfigScope,
5
+ normalizeProjectDeclaration,
6
+ selectProfilePlan
7
+ } = require('./config-plan.js');
8
+ const {
9
+ CONFIG_FACTORY_CONTRACT_VERSION,
10
+ CONFIG_SCOPE_CONTRACT_VERSION,
11
+ defineConfig,
12
+ isConfigFactory
13
+ } = require('./config-factory.js');
14
+
15
+ const PULSE_PACKAGE_API_VERSION = 'pulse.package-authoring.v1';
16
+
17
+ function evaluateConfigFactory(factory, options = {}) {
18
+ if (!isConfigFactory(factory)) {
19
+ const error = new TypeError('Pulse config must export defineConfig((scope) => ({ ... })).');
20
+ error.code = 'PULSE_CONFIG_FACTORY_REQUIRED';
21
+ throw error;
22
+ }
23
+ const raw = factory(createConfigScope());
24
+ if (raw && typeof raw.then === 'function') {
25
+ const error = new TypeError('Pulse config factories must be synchronous.');
26
+ error.code = 'PULSE_CONFIG_FACTORY_ASYNC_UNSUPPORTED';
27
+ throw error;
28
+ }
29
+ const declaration = normalizeProjectDeclaration(raw, { source: options.source });
30
+ const plan = selectProfilePlan(declaration, {
31
+ cliProfile: options.cliProfile,
32
+ environmentProfile: options.environmentProfile,
33
+ name: options.profile,
34
+ source: options.profileSource
35
+ });
36
+ return Object.freeze({ declaration, plan });
37
+ }
38
+
39
+ module.exports = Object.freeze({
40
+ PULSE_PACKAGE_API_VERSION,
41
+ CONFIG_FACTORY_CONTRACT_VERSION,
42
+ CONFIG_SCOPE_CONTRACT_VERSION,
43
+ defineConfig,
44
+ isConfigFactory,
45
+ evaluateConfigFactory
46
+ });