@stream44.studio/encapsulate 0.4.0-rc.38 → 0.4.0-rc.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  <table>
2
2
  <tr>
3
- <td><a href="https://Stream44.Studio"><img src=".o/stream44.studio/assets/Icon-v1.svg" width="42" height="42"></a></td>
4
- <td><strong><a href="https://Stream44.Studio">Stream44 Studio</a></strong><br/>Open Development Project</td>
3
+ <td><a href="https://Stream44.Systems"><img src=".o/stream44.studio/assets/Icon-v1.svg" width="42" height="42"></a></td>
4
+ <td><strong><a href="https://Stream44.Systems">Stream44 Systems</a></strong><br/>Open Development Project</td>
5
5
  <td>Preview release for community feedback.<br/>Get in touch on <a href="https://discord.gg/9eBcQXEJAN">discord</a>.</td>
6
6
  <td>Designed by Hand<br/><b>AI assisted Code</a></td>
7
7
  </tr>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stream44.studio/encapsulate",
3
- "version": "0.4.0-rc.38",
3
+ "version": "0.4.0-rc.42",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -10,9 +10,9 @@
10
10
  "type": "module",
11
11
  "exports": {
12
12
  "./encapsulate": "./src/encapsulate.ts",
13
- "./spine-contracts/CapsuleSpineContract.v0/Static.v0": "./src/spine-contracts/CapsuleSpineContract.v0/Static.v0.ts",
14
- "./spine-contracts/CapsuleSpineContract.v0/Membrane.v0": "./src/spine-contracts/CapsuleSpineContract.v0/Membrane.v0.ts",
15
- "./spine-factories/CapsuleSpineFactory.v0": "./src/spine-factories/CapsuleSpineFactory.v0.ts",
13
+ "./spine-contracts/CapsuleSpineContract.v0/Static": "./src/spine-contracts/CapsuleSpineContract.v0/Static.ts",
14
+ "./spine-contracts/CapsuleSpineContract.v0/Membrane": "./src/spine-contracts/CapsuleSpineContract.v0/Membrane.ts",
15
+ "./spine-factories/CapsuleSpineFactory": "./src/spine-factories/CapsuleSpineFactory.ts",
16
16
  "./spine-factories/TimingObserver": "./src/spine-factories/TimingObserver.ts",
17
17
  "./structs/Capsule": "./structs/Capsule.ts",
18
18
  "./structs/CapsuleProjectionContext": "./structs/CapsuleProjectionContext.ts"
@@ -50,7 +50,7 @@ async function constructCacheFilePath(moduleFilepath: string, importStackLine: n
50
50
  * Finds the nearest package.json and constructs an npm URI for cache files.
51
51
  * First checks if the path contains /node_modules/ and if so, extracts the portion
52
52
  * after the last /node_modules/ occurrence for consistent paths in dev and installed mode.
53
- * This matches the logic from static-analyzer.v0.ts
53
+ * This matches the logic from static-analyzer.ts
54
54
  */
55
55
  async function constructNpmUriForCache(absoluteFilepath: string, spineRoot: string): Promise<string | null> {
56
56
  // Check for /node_modules/ in the path — use the last occurrence to handle nested node_modules
@@ -742,7 +742,7 @@ export function CapsuleModuleProjector({
742
742
  const hasStandalone = hasStandaloneProperty(capsule, spineContractUri)
743
743
 
744
744
  // Add runtime imports for standalone functions
745
- const runtimeImport = hasStandalone ? `"use client"\nimport { Spine, SpineRuntime } from '@stream44.studio/encapsulate/encapsulate'\nimport { CapsuleSpineContract } from '@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0/Membrane.v0'\n` : ''
745
+ const runtimeImport = hasStandalone ? `"use client"\nimport { Spine, SpineRuntime } from '@stream44.studio/encapsulate/encapsulate'\nimport { CapsuleSpineContract } from '@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0/Membrane'\n` : ''
746
746
 
747
747
  // Generate default export based on capsule type
748
748
  let defaultExport = ''
@@ -1072,7 +1072,7 @@ ${defaultExport}
1072
1072
  const mappedCapsuleExpression = rewriteCapsuleExpressionWithCST(mappedCapsule)
1073
1073
 
1074
1074
  // Add runtime imports for standalone functions
1075
- const mappedRuntimeImport = mappedHasStandalone ? `"use client"\nimport { Spine, SpineRuntime } from '@stream44.studio/encapsulate/encapsulate'\nimport { CapsuleSpineContract } from '@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0/Membrane.v0'\n` : ''
1075
+ const mappedRuntimeImport = mappedHasStandalone ? `"use client"\nimport { Spine, SpineRuntime } from '@stream44.studio/encapsulate/encapsulate'\nimport { CapsuleSpineContract } from '@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0/Membrane'\n` : ''
1076
1076
 
1077
1077
  let mappedDefaultExport = ''
1078
1078
  if (mappedHasStandalone) {
@@ -1,4 +1,6 @@
1
1
 
2
+ import type { TimingObserverInterface } from './spine-factories/TimingObserver'
3
+
2
4
  // CACHE_BUST_VERSION: Increment this whenever CST cache must be invalidated due to structural changes
3
5
  // This ensures projected capsules are regenerated when the CST format changes
4
6
  const CACHE_BUST_VERSION = 22
@@ -7,7 +9,7 @@ type TSpineOptions = {
7
9
  spineFilesystemRoot?: string,
8
10
  spineContracts: Record<string, any>,
9
11
  staticAnalyzer?: any,
10
- timing?: { record: (step: string) => void, chalk?: any },
12
+ timing?: TimingObserverInterface,
11
13
  projectionContext?: {
12
14
  capsuleModuleProjectionPackage?: string,
13
15
  capsuleModuleProjectionRoot?: string,
@@ -56,6 +58,9 @@ type TCapsuleSnapshot = {
56
58
  type TCapsuleMakeInstanceOptions = {
57
59
  overrides?: Record<string, any>,
58
60
  options?: Record<string, any>,
61
+ /** Nested capsule-name-targeted options from a parent mapping.
62
+ * Merged AFTER options so they take precedence over intermediate defaults. */
63
+ transitiveOverrides?: Record<string, any>,
59
64
  runtimeSpineContracts?: Record<string, any>,
60
65
  sharedSelf?: Record<string, any>,
61
66
  rootCapsule?: {
@@ -65,7 +70,8 @@ type TCapsuleMakeInstanceOptions = {
65
70
  },
66
71
  parentCapsuleSourceUriLineRefInstanceId?: string,
67
72
  sit?: { capsuleInstances: Record<string, { capsuleName: string, capsuleSourceUriLineRef: string, parentCapsuleSourceUriLineRefInstanceId: string }> },
68
- skipCache?: boolean
73
+ skipCache?: boolean,
74
+ freezePhase?: boolean
69
75
  }
70
76
 
71
77
  type TCapsule = {
@@ -115,6 +121,7 @@ type TSpineContext = {
115
121
  export const CapsulePropertyTypes = {
116
122
  Function: 'Function' as const,
117
123
  GetterFunction: 'GetterFunction' as const,
124
+ ConstantGetterFunction: 'ConstantGetterFunction' as const,
118
125
  SetterFunction: 'SetterFunction' as const,
119
126
  String: 'String' as const,
120
127
  Mapping: 'Mapping' as const,
@@ -125,6 +132,7 @@ export const CapsulePropertyTypes = {
125
132
  Init: 'Init' as const,
126
133
  Dispose: 'Dispose' as const,
127
134
  OnFreeze: 'OnFreeze' as const,
135
+ ProxyFunction: 'ProxyFunction' as const,
128
136
  }
129
137
 
130
138
  // ##################################################
@@ -198,8 +206,8 @@ export async function SpineRuntime(options: TSpineRuntimeOptions): Promise<TSpin
198
206
 
199
207
  // If the value is a raw capsule instance (has spineContractCapsuleInstances
200
208
  // but is NOT a Proxy that handles API access), unwrap it
201
- // Static.v0 sets apiTarget[property.name] = mappedInstance (raw)
202
- // Membrane.v0 sets apiTarget[property.name] = new Proxy(...) which handles API access
209
+ // Static sets apiTarget[property.name] = mappedInstance (raw)
210
+ // Membrane sets apiTarget[property.name] = new Proxy(...) which handles API access
203
211
  if (value && typeof value === 'object' && value.spineContractCapsuleInstances) {
204
212
  // Check if this is a raw capsule instance by seeing if it has .api
205
213
  // and the .api doesn't have the same spineContractCapsuleInstances
@@ -459,7 +467,7 @@ export async function Spine(options: TSpineOptions): Promise<TSpine> {
459
467
  snapshot.capsules[capsule.cst.source.capsuleName] = snapshot.capsules[capsuleSourceLineRef]
460
468
  }
461
469
 
462
- const capsuleInstance = await capsule.makeInstance()
470
+ const capsuleInstance = await capsule.makeInstance({ freezePhase: true })
463
471
 
464
472
  // Run OnFreeze functions so capsules can perform side effects
465
473
  // (e.g. file projection) at build/freeze time
@@ -531,7 +539,7 @@ export async function Spine(options: TSpineOptions): Promise<TSpine> {
531
539
  const mappedProjectionPath = mappedInstance.mappedPropertyName?.startsWith('/') ? mappedInstance.mappedPropertyName : projectionPath
532
540
  // For regular mapped capsules (non-struct delegates), use their own CST as
533
541
  // parentCapsuleCst so nested property contract delegates see the correct parent.
534
- // Property contract delegates (flagged by Static.v0) pass through the current
542
+ // Property contract delegates (flagged by Static) pass through the current
535
543
  // parentCapsuleCst so their OnFreeze sees the declaring capsule's CST.
536
544
  const mappedCapsule = (!mappedInstance.isPropertyContractDelegate && mappedInstance.capsuleName) ? capsules[mappedInstance.capsuleName] : undefined
537
545
  const childParentCst = mappedCapsule?.cst || parentCapsuleCst
@@ -665,7 +673,9 @@ async function encapsulate(definition: TCapsuleDefinition, options: TCapsuleOpti
665
673
  encapsulateOptions,
666
674
  cst,
667
675
  crt: crts?.[capsuleSourceLineRef],
668
- makeInstance: async ({ overrides = {}, options = {}, runtimeSpineContracts, sharedSelf, rootCapsule, parentCapsuleSourceUriLineRefInstanceId, sit, skipCache }: TCapsuleMakeInstanceOptions = {}) => {
676
+ makeInstance: async ({ overrides = {}, options = {}, transitiveOverrides, runtimeSpineContracts, sharedSelf, rootCapsule, parentCapsuleSourceUriLineRefInstanceId, sit, skipCache, freezePhase }: TCapsuleMakeInstanceOptions = {}) => {
677
+
678
+ spine.spineOptions.timing?.record(`makeInstance: ${capsuleName || capsuleSourceLineRef}${freezePhase ? ' [freeze]' : ''}${sharedSelf ? ' [extends]' : ''}`)
669
679
 
670
680
  // Create cache key based on parameters
671
681
  // When sharedSelf is provided, we must NOT cache because each extending capsule
@@ -673,15 +683,14 @@ async function encapsulate(definition: TCapsuleDefinition, options: TCapsuleOpti
673
683
  // This is critical for the pattern where multiple structs extend the same parent.
674
684
  // When skipCache is true (property contract delegates like structs/Capsule),
675
685
  // each parent capsule must get its own unique instance.
676
- const cacheKey = (sharedSelf || skipCache) ? null : JSON.stringify({
677
- overrides,
678
- options,
679
- hasRuntimeContracts: !!runtimeSpineContracts
680
- })
686
+ const cacheKey = (sharedSelf || skipCache) ? null : computeCacheKey(
687
+ overrides, options, transitiveOverrides, !!runtimeSpineContracts
688
+ )
681
689
 
682
690
  // Check if we already have a pending or completed instance creation
683
691
  // Skip cache when sharedSelf is provided (cacheKey is null)
684
692
  if (cacheKey && instanceCache.has(cacheKey)) {
693
+ spine.spineOptions.timing?.record(`makeInstance: CACHE HIT ${capsuleName || capsuleSourceLineRef}`)
685
694
  return instanceCache.get(cacheKey)!
686
695
  }
687
696
 
@@ -723,6 +732,7 @@ async function encapsulate(definition: TCapsuleDefinition, options: TCapsuleOpti
723
732
  const propDefTyped = propDef as Record<string, any>
724
733
  const aliasName = propDefTyped.as
725
734
  let delegateOptions = propDefTyped.options
735
+ const delegateDepends = propDefTyped.depends
726
736
  const contractKey = aliasName || ('#' + propContractUri.substring(1))
727
737
 
728
738
  if (!propertyContractDefinitions[spineContractUri]['#']) {
@@ -738,6 +748,9 @@ async function encapsulate(definition: TCapsuleDefinition, options: TCapsuleOpti
738
748
  value: delegateCapsuleObj || delegateUri,
739
749
  propertyContractDelegate: propContractUri,
740
750
  as: aliasName,
751
+ // Forward depends from the struct declaration so the delegate
752
+ // options function can access the required parent properties
753
+ depends: delegateDepends,
741
754
  // Pass options from the property contract delegate to the mapped capsule
742
755
  delegateOptions
743
756
  }
@@ -774,12 +787,19 @@ async function encapsulate(definition: TCapsuleDefinition, options: TCapsuleOpti
774
787
  }
775
788
  }
776
789
 
777
- // Merge in order: overrides by lineRef, overrides by name, options
790
+ // Merge in order: overrides, options, then transitive overrides.
791
+ // - Overrides (from run()) are general defaults — options are more specific.
792
+ // - Transitive overrides (nested capsule-name-targeted options from a parent)
793
+ // are the most specific and win over intermediate capsule options.
778
794
  mergeByContract(overrides?.[encapsulateOptions.capsuleSourceLineRef], 'Overrides')
779
795
  if (encapsulateOptions.capsuleName) {
780
796
  mergeByContract(overrides?.[encapsulateOptions.capsuleName], 'Overrides')
781
797
  }
782
798
  mergeByContract(options, 'Options')
799
+ mergeByContract(transitiveOverrides?.[encapsulateOptions.capsuleSourceLineRef], 'TransitiveOverrides')
800
+ if (encapsulateOptions.capsuleName) {
801
+ mergeByContract(transitiveOverrides?.[encapsulateOptions.capsuleName], 'TransitiveOverrides')
802
+ }
783
803
 
784
804
  // Extract default values from property definitions (Literal/String types)
785
805
  // This ensures child capsule's default values are available before parent is instantiated
@@ -789,7 +809,8 @@ async function encapsulate(definition: TCapsuleDefinition, options: TCapsuleOpti
789
809
  if (propertyContractUri !== '#') continue
790
810
  for (const [propertyName, propertyDef] of Object.entries(properties as Record<string, any>)) {
791
811
  if (propertyDef.type === CapsulePropertyTypes.Literal ||
792
- propertyDef.type === CapsulePropertyTypes.String) {
812
+ propertyDef.type === CapsulePropertyTypes.String ||
813
+ propertyDef.type === CapsulePropertyTypes.Constant) {
793
814
  if (propertyDef.value !== undefined) {
794
815
  defaultPropertyValues[propertyName] = propertyDef.value
795
816
  }
@@ -846,6 +867,7 @@ async function encapsulate(definition: TCapsuleDefinition, options: TCapsuleOpti
846
867
  capsuleSourceLineRef: absoluteCapsuleSourceLineRef,
847
868
  capsuleSourceNameRefHash: cst?.capsuleSourceNameRefHash,
848
869
  moduleFilepath: originalAbsoluteModuleFilepath,
870
+ spineFilesystemRoot: spine.spineOptions.spineFilesystemRoot,
849
871
  // Root capsule metadata will be populated after extends chain is resolved
850
872
  rootCapsule: {
851
873
  capsuleName: undefined as string | undefined,
@@ -925,9 +947,11 @@ async function encapsulate(definition: TCapsuleDefinition, options: TCapsuleOpti
925
947
  })
926
948
  }
927
949
 
950
+ spine.spineOptions.timing?.record(`makeInstance: extends → ${extendsCapsule.encapsulateOptions?.capsuleName || '?'}`)
928
951
  extendedCapsuleInstance = await extendsCapsule.makeInstance({
929
952
  overrides,
930
953
  options,
954
+ transitiveOverrides,
931
955
  runtimeSpineContracts,
932
956
  sharedSelf: self,
933
957
  rootCapsule: rootCapsule || {
@@ -936,9 +960,10 @@ async function encapsulate(definition: TCapsuleDefinition, options: TCapsuleOpti
936
960
  moduleFilepath: absoluteModuleFilepath
937
961
  },
938
962
  parentCapsuleSourceUriLineRefInstanceId: parentCapsuleSourceUriLineRefInstanceId
939
- ? await sha256(parentCapsuleSourceUriLineRefInstanceId + ':' + (cst?.capsuleSourceUriLineRef || encapsulateOptions.capsuleSourceLineRef))
940
- : await sha256(cst?.capsuleSourceUriLineRef || encapsulateOptions.capsuleSourceLineRef),
941
- sit
963
+ ? sha256(parentCapsuleSourceUriLineRefInstanceId + ':' + (cst?.capsuleSourceUriLineRef || encapsulateOptions.capsuleSourceLineRef))
964
+ : sha256(cst?.capsuleSourceUriLineRef || encapsulateOptions.capsuleSourceLineRef),
965
+ sit,
966
+ freezePhase
942
967
  })
943
968
 
944
969
  // Propagate this (child) capsule's encapsulatedApi to all parent spine contract
@@ -974,8 +999,8 @@ async function encapsulate(definition: TCapsuleDefinition, options: TCapsuleOpti
974
999
  // child: sha256(parentCapsuleSourceUriLineRefInstanceId + ":" + capsuleSourceUriLineRef)
975
1000
  const capsuleSourceUriLineRef = cst?.capsuleSourceUriLineRef || encapsulateOptions.capsuleSourceLineRef
976
1001
  const capsuleSourceUriLineRefInstanceId = parentCapsuleSourceUriLineRefInstanceId
977
- ? await sha256(parentCapsuleSourceUriLineRefInstanceId + ':' + capsuleSourceUriLineRef)
978
- : await sha256(capsuleSourceUriLineRef)
1002
+ ? sha256(parentCapsuleSourceUriLineRefInstanceId + ':' + capsuleSourceUriLineRef)
1003
+ : sha256(capsuleSourceUriLineRef)
979
1004
 
980
1005
  // Register this instance in the sit structure if provided
981
1006
  if (sit) {
@@ -1000,7 +1025,8 @@ async function encapsulate(definition: TCapsuleDefinition, options: TCapsuleOpti
1000
1025
  capsuleSourceUriLineRefInstanceId,
1001
1026
  capsuleName: encapsulateOptions.capsuleName,
1002
1027
  capsuleSourceUriLineRef,
1003
- sit
1028
+ sit,
1029
+ freezePhase
1004
1030
  }
1005
1031
 
1006
1032
  // Set capsule metadata struct on self early so it's available in options() callbacks during mapping
@@ -1011,6 +1037,14 @@ async function encapsulate(definition: TCapsuleDefinition, options: TCapsuleOpti
1011
1037
  }
1012
1038
  ownSelf['#@stream44.studio/encapsulate/structs/Capsule'] = capsuleMetadataStruct
1013
1039
 
1040
+ // Promote metadata values to top-level self properties so struct delegate
1041
+ // proxies (which read from the capsule instance's own self/api) can access them.
1042
+ for (const [metaKey, metaValue] of Object.entries(capsuleMetadataStruct)) {
1043
+ if (metaKey !== 'rootCapsule' && metaValue !== undefined && self[metaKey] === undefined) {
1044
+ self[metaKey] = metaValue
1045
+ }
1046
+ }
1047
+
1014
1048
  // Use runtime spine contracts if provided, otherwise fall back to encapsulation spine contracts
1015
1049
  const activeSpineContracts = runtimeSpineContracts || spine.spineContracts
1016
1050
 
@@ -1055,6 +1089,7 @@ async function encapsulate(definition: TCapsuleDefinition, options: TCapsuleOpti
1055
1089
  await spineContractCapsuleInstance.mapProperty({
1056
1090
  overrides,
1057
1091
  options,
1092
+ transitiveOverrides,
1058
1093
  property: {
1059
1094
  name: propertyName,
1060
1095
  definition: propertyDefinition,
@@ -1229,15 +1264,41 @@ function relative(from: string, to: string): string {
1229
1264
  return result || '.'
1230
1265
  }
1231
1266
 
1232
- async function sha256(input: string): Promise<string> {
1233
- const data = new TextEncoder().encode(input)
1234
- const hashBuffer = await crypto.subtle.digest('SHA-256', data)
1235
- const hashArray = new Uint8Array(hashBuffer)
1236
- let hex = ''
1237
- for (let i = 0; i < hashArray.length; i++) {
1238
- hex += hashArray[i].toString(16).padStart(2, '0')
1267
+ // Fast cache key computation for makeInstance — avoids JSON.stringify for the common empty case
1268
+ const _emptyObj = {}
1269
+ const _cacheKeyEmpty = '{"overrides":{},"options":{},"hasRuntimeContracts":false}'
1270
+ const _cacheKeyEmptyWithRuntime = '{"overrides":{},"options":{},"hasRuntimeContracts":true}'
1271
+ function _isEmptyObj(obj: any): boolean { for (const _ in obj) return false; return true }
1272
+ const _cacheKeyLastRef = { overrides: _emptyObj as any, options: _emptyObj as any, transitiveOverrides: undefined as any, hasRuntime: false, key: _cacheKeyEmpty }
1273
+ function computeCacheKey(overrides: any, options: any, transitiveOverrides: any, hasRuntimeContracts: boolean): string {
1274
+ // Reference-equality shortcut: if same objects as last call, reuse key
1275
+ if (overrides === _cacheKeyLastRef.overrides && options === _cacheKeyLastRef.options
1276
+ && transitiveOverrides === _cacheKeyLastRef.transitiveOverrides && hasRuntimeContracts === _cacheKeyLastRef.hasRuntime) {
1277
+ return _cacheKeyLastRef.key
1239
1278
  }
1240
- return hex
1279
+ // Fast-path: empty overrides + empty options + no transitive = constant string
1280
+ if (!transitiveOverrides && _isEmptyObj(overrides) && _isEmptyObj(options)) {
1281
+ const key = hasRuntimeContracts ? _cacheKeyEmptyWithRuntime : _cacheKeyEmpty
1282
+ _cacheKeyLastRef.overrides = overrides; _cacheKeyLastRef.options = options
1283
+ _cacheKeyLastRef.transitiveOverrides = transitiveOverrides; _cacheKeyLastRef.hasRuntime = hasRuntimeContracts
1284
+ _cacheKeyLastRef.key = key
1285
+ return key
1286
+ }
1287
+ const key = JSON.stringify({ overrides, options, transitiveOverrides, hasRuntimeContracts })
1288
+ _cacheKeyLastRef.overrides = overrides; _cacheKeyLastRef.options = options
1289
+ _cacheKeyLastRef.transitiveOverrides = transitiveOverrides; _cacheKeyLastRef.hasRuntime = hasRuntimeContracts
1290
+ _cacheKeyLastRef.key = key
1291
+ return key
1292
+ }
1293
+
1294
+ const _sha256Cache = new Map<string, string>()
1295
+ const _createHash = require('crypto').createHash
1296
+ function sha256(input: string): string {
1297
+ let result = _sha256Cache.get(input)
1298
+ if (result !== undefined) return result
1299
+ result = _createHash('sha256').update(input).digest('hex') as string
1300
+ _sha256Cache.set(input, result)
1301
+ return result
1241
1302
  }
1242
1303
 
1243
1304
  function isObject(item: any): boolean {
@@ -1,5 +1,6 @@
1
1
  import { CapsulePropertyTypes } from "../../encapsulate"
2
- import { ContractCapsuleInstanceFactory, CapsuleInstanceRegistry } from "./Static.v0"
2
+ import { ContractCapsuleInstanceFactory, CapsuleInstanceRegistry } from "./Static"
3
+ import type { TimingObserverInterface } from "../../spine-factories/TimingObserver"
3
4
 
4
5
  type CallerContext = {
5
6
  capsuleSourceLineRef: string
@@ -86,7 +87,8 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
86
87
  runtimeSpineContracts,
87
88
  instanceRegistry,
88
89
  extendedCapsuleInstance,
89
- capsuleInstance
90
+ capsuleInstance,
91
+ timing
90
92
  }: {
91
93
  spineContractUri: string
92
94
  capsule: any
@@ -109,8 +111,9 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
109
111
  instanceRegistry?: CapsuleInstanceRegistry
110
112
  extendedCapsuleInstance?: any
111
113
  capsuleInstance?: any
114
+ timing?: TimingObserverInterface
112
115
  }) {
113
- super({ spineContractUri, capsule, self, ownSelf, encapsulatedApi, resolve, importCapsule, spineFilesystemRoot, freezeCapsule, instanceRegistry, extendedCapsuleInstance, capsuleInstance })
116
+ super({ spineContractUri, capsule, self, ownSelf, encapsulatedApi, resolve, importCapsule, spineFilesystemRoot, freezeCapsule, instanceRegistry, extendedCapsuleInstance, capsuleInstance, timing })
114
117
  this.getEventIndex = getEventIndex
115
118
  this.incrementEventIndex = incrementEventIndex
116
119
  this.getCurrentCallerContext = getCurrentCallerContext
@@ -137,7 +140,7 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
137
140
  return ctx
138
141
  }
139
142
 
140
- protected async mapMappingProperty({ overrides, options, property }: { overrides: any, options: any, property: any }) {
143
+ protected async mapMappingProperty({ overrides, options, transitiveOverrides, property }: { overrides: any, options: any, transitiveOverrides?: any, property: any }) {
141
144
 
142
145
  const mappedCapsule = await this.resolveMappedCapsule({ property })
143
146
  const constants = await this.extractConstants({ mappedCapsule })
@@ -151,9 +154,23 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
151
154
  const minimalSelf = this.self[capsuleStructKey]
152
155
  ? { [capsuleStructKey]: this.self[capsuleStructKey] }
153
156
  : {}
154
- const mappingOptions = property.definition.delegateOptions
155
- || (typeof optionsFn === 'function'
156
- ? await optionsFn({ self: property.definition.depends ? this.self : minimalSelf, constants })
157
+ // During freeze phase, skip calling function-based options callbacks — runtime
158
+ // overrides haven't been applied so self.* references may be incomplete.
159
+ // Use a truthy placeholder so the instance registry still creates a fresh instance.
160
+ const isFreezePhase = !!this.capsuleInstance?.freezePhase
161
+ const hasFunctionOptions = typeof optionsFn === 'function'
162
+ const selfArg = { self: property.definition.depends ? this.self : minimalSelf, constants }
163
+ const delegateOpts = property.definition.delegateOptions
164
+ const resolvedDelegateOptions = delegateOpts
165
+ ? (typeof delegateOpts === 'function'
166
+ ? (isFreezePhase ? {} : await delegateOpts(selfArg))
167
+ : delegateOpts)
168
+ : undefined
169
+ const mappingOptions = resolvedDelegateOptions
170
+ || (hasFunctionOptions
171
+ ? (isFreezePhase
172
+ ? {} // truthy placeholder
173
+ : await optionsFn(selfArg))
157
174
  : optionsFn)
158
175
 
159
176
  // Check for existing instance in registry - reuse if available (regardless of options)
@@ -270,13 +287,14 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
270
287
  }
271
288
  }
272
289
 
273
- // Merge nested capsule-name-targeted options into overrides
274
- // These will be picked up when child capsules with matching names are instantiated
290
+ // Build transitive overrides for the child: merge any inherited transitive
291
+ // overrides with this mapping's nested capsule-name-targeted options.
292
+ let mappedTransitiveOverrides: Record<string, any> | undefined = transitiveOverrides
275
293
  if (nestedCapsuleOptions) {
276
- mappedOverrides = { ...mappedOverrides }
294
+ mappedTransitiveOverrides = { ...(mappedTransitiveOverrides || {}) }
277
295
  for (const [capsuleNameKey, capsuleOptions] of Object.entries(nestedCapsuleOptions)) {
278
- mappedOverrides[capsuleNameKey] = {
279
- ...(mappedOverrides[capsuleNameKey] || {}),
296
+ mappedTransitiveOverrides[capsuleNameKey] = {
297
+ ...(mappedTransitiveOverrides[capsuleNameKey] || {}),
280
298
  ...capsuleOptions
281
299
  }
282
300
  }
@@ -285,11 +303,13 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
285
303
  const mappedCapsuleInstance = await mappedCapsule.makeInstance({
286
304
  overrides: mappedOverrides,
287
305
  options: ownMappingOptions,
306
+ transitiveOverrides: mappedTransitiveOverrides,
288
307
  runtimeSpineContracts: this.runtimeSpineContracts,
289
308
  rootCapsule: this.capsuleInstance?.rootCapsule,
290
309
  parentCapsuleSourceUriLineRefInstanceId: this.capsuleInstance?.capsuleSourceUriLineRefInstanceId,
291
310
  sit: this.capsuleInstance?.sit,
292
- skipCache: isCapsuleStruct
311
+ skipCache: isCapsuleStruct,
312
+ freezePhase: this.capsuleInstance?.freezePhase || undefined
293
313
  })
294
314
 
295
315
  // Register the instance (replaces null pre-registration marker)
@@ -481,6 +501,51 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
481
501
  })
482
502
  }
483
503
 
504
+ protected override mapConstantGetterFunctionProperty({ property }: { property: any }) {
505
+ const value = property.definition.value({ constants: this.self })
506
+
507
+ // Store eagerly on self like a Constant
508
+ this.self[property.name] = value
509
+ if (this.ownSelf) {
510
+ this.ownSelf[property.name] = value
511
+ }
512
+
513
+ // Wrap with membrane event tracking (read-only like Constant)
514
+ Object.defineProperty(this.encapsulatedApi, property.name, {
515
+ get: () => {
516
+ const currentValue = this.self[property.name]
517
+
518
+ const event: any = {
519
+ event: 'get',
520
+ eventIndex: this.incrementEventIndex(),
521
+ membrane: 'external',
522
+ target: {
523
+ capsuleSourceLineRef: this.encapsulateOptions.capsuleSourceLineRef,
524
+ spineContractCapsuleInstanceId: this.id,
525
+ prop: property.name,
526
+ },
527
+ value: currentValue
528
+ }
529
+
530
+ if (this.capsuleSourceNameRef) {
531
+ event.target.capsuleSourceNameRef = this.capsuleSourceNameRef
532
+ }
533
+ if (this.capsuleSourceNameRefHash) {
534
+ event.target.capsuleSourceNameRefHash = this.capsuleSourceNameRefHash
535
+ }
536
+
537
+ this.addCallerContextToEvent(event)
538
+ this.onMembraneEvent?.(event)
539
+ return currentValue
540
+ },
541
+ set: () => {
542
+ throw new Error(`Cannot set ConstantGetterFunction property '${property.name}'`)
543
+ },
544
+ enumerable: true,
545
+ configurable: true
546
+ })
547
+ }
548
+
484
549
  protected mapFunctionProperty({ property }: { property: any }) {
485
550
  const selfProxy = this.createSelfProxy()
486
551
  const boundFunction = property.definition.value.bind(selfProxy)
@@ -620,6 +685,86 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
620
685
  })
621
686
  }
622
687
 
688
+ protected override mapProxyFunctionProperty({ property }: { property: any }) {
689
+ const selfProxy = this.createSelfProxy()
690
+
691
+ const childTargetFn = property.definition.value.target
692
+ const childInvokeFn = property.definition.value.invoke
693
+
694
+ // Inherit missing parts from parent's ProxyFunction (if extending)
695
+ const parentParts = this.self[`__proxyFn_${property.name}`]
696
+ const targetFn = childTargetFn ?? parentParts?.target
697
+ const invokeFn = childInvokeFn ?? parentParts?.invoke
698
+
699
+ if (!targetFn) throw new Error(`ProxyFunction '${property.name}': target() is required`)
700
+ if (!invokeFn) throw new Error(`ProxyFunction '${property.name}': invoke() is required`)
701
+
702
+ // Store parts for potential child override
703
+ this.self[`__proxyFn_${property.name}`] = { target: targetFn, invoke: invokeFn }
704
+
705
+ const boundProxyFn = (...args: any[]) => {
706
+ const transformedArgs = invokeFn.call(selfProxy, ...args)
707
+ const target = targetFn.call(selfProxy)
708
+ if (transformedArgs && typeof transformedArgs.then === 'function') {
709
+ return transformedArgs.then((resolved: any) => target(resolved))
710
+ }
711
+ return target(transformedArgs)
712
+ }
713
+
714
+ Object.defineProperty(this.encapsulatedApi, property.name, {
715
+ get: () => {
716
+ return (...args: any[]) => {
717
+ const callEvent: any = {
718
+ event: 'call',
719
+ eventIndex: this.incrementEventIndex(),
720
+ membrane: 'external',
721
+ target: {
722
+ capsuleSourceLineRef: this.encapsulateOptions.capsuleSourceLineRef,
723
+ spineContractCapsuleInstanceId: this.id,
724
+ prop: property.name,
725
+ },
726
+ args
727
+ }
728
+
729
+ if (this.capsuleSourceNameRef) {
730
+ callEvent.target.capsuleSourceNameRef = this.capsuleSourceNameRef
731
+ }
732
+ if (this.capsuleSourceNameRefHash) {
733
+ callEvent.target.capsuleSourceNameRefHash = this.capsuleSourceNameRefHash
734
+ }
735
+ if (this.capsuleInstance?.capsuleSourceUriLineRefInstanceId) {
736
+ callEvent.target.capsuleSourceUriLineRefInstanceId = this.capsuleInstance.capsuleSourceUriLineRefInstanceId
737
+ }
738
+
739
+ this.addCallerContextToEvent(callEvent)
740
+ this.onMembraneEvent?.(callEvent)
741
+
742
+ const previousCallerContext = this.getCurrentCallerContext()
743
+ this.setCurrentCallerContext(this.buildCallerContext(property.name))
744
+ const result = boundProxyFn(...args)
745
+ this.setCurrentCallerContext(previousCallerContext)
746
+
747
+ const resultEvent: any = {
748
+ event: 'call-result',
749
+ eventIndex: this.incrementEventIndex(),
750
+ membrane: 'external',
751
+ callEventIndex: callEvent.eventIndex,
752
+ target: {
753
+ spineContractCapsuleInstanceId: this.id,
754
+ },
755
+ result
756
+ }
757
+
758
+ this.onMembraneEvent?.(resultEvent)
759
+
760
+ return result
761
+ }
762
+ },
763
+ enumerable: true,
764
+ configurable: true
765
+ })
766
+ }
767
+
623
768
  protected mapGetterFunctionProperty({ property }: { property: any }) {
624
769
  const getterFn = property.definition.value
625
770
  const selfProxy = this.createSelfProxy()
@@ -749,16 +894,13 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
749
894
  }
750
895
 
751
896
  // Determine the value source and get the value
897
+ // Virtual dispatch: child overrides take precedence over both
898
+ // self and own encapsulatedApi, matching class-inheritance semantics
752
899
  let value: any
753
900
  let source: 'self' | 'encapsulatedApi' | 'childApi' | 'extendedApi' | undefined
754
901
 
755
- if (prop in target) {
756
- value = target[prop]
757
- source = 'self'
758
- } else if (prop in factory.encapsulatedApi) {
759
- value = factory.encapsulatedApi[prop]
760
- source = 'encapsulatedApi'
761
- } else if (factory.childEncapsulatedApis) {
902
+ // Check child capsule APIs first for virtual dispatch
903
+ if (factory.childEncapsulatedApis) {
762
904
  for (const childApi of factory.childEncapsulatedApis) {
763
905
  if (prop in childApi) {
764
906
  value = childApi[prop]
@@ -768,6 +910,16 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
768
910
  }
769
911
  }
770
912
 
913
+ if (source === undefined) {
914
+ if (prop in target) {
915
+ value = target[prop]
916
+ source = 'self'
917
+ } else if (prop in factory.encapsulatedApi) {
918
+ value = factory.encapsulatedApi[prop]
919
+ source = 'encapsulatedApi'
920
+ }
921
+ }
922
+
771
923
  if (source === undefined && extendedApi && prop in extendedApi) {
772
924
  value = extendedApi[prop]
773
925
  source = 'extendedApi'
@@ -914,7 +1066,8 @@ export function CapsuleSpineContract({
914
1066
  spineFilesystemRoot,
915
1067
  resolve,
916
1068
  importCapsule,
917
- npmUriForFilepath
1069
+ npmUriForFilepath,
1070
+ timing
918
1071
  }: {
919
1072
  onMembraneEvent?: (event: any) => void
920
1073
  freezeCapsule?: (capsule: any) => Promise<any>
@@ -923,6 +1076,7 @@ export function CapsuleSpineContract({
923
1076
  resolve?: (uri: string, parentFilepath: string) => Promise<string>
924
1077
  importCapsule?: (filepath: string) => Promise<any>
925
1078
  npmUriForFilepath?: (filepath: string) => Promise<string | null>
1079
+ timing?: TimingObserverInterface
926
1080
  } = {}) {
927
1081
 
928
1082
  let eventIndex = 0
@@ -983,7 +1137,8 @@ export function CapsuleSpineContract({
983
1137
  runtimeSpineContracts,
984
1138
  instanceRegistry,
985
1139
  extendedCapsuleInstance,
986
- capsuleInstance
1140
+ capsuleInstance,
1141
+ timing
987
1142
  })
988
1143
  },
989
1144
  hydrate: ({ capsuleSnapshot }: { capsuleSnapshot: any }): any => {