@stream44.studio/encapsulate 0.4.0-rc.19 → 0.4.0-rc.21

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/LICENSE.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- encapsulate: An experimental implementation of the https://PrivateData.Space model in TypeScript.
3
+ encapsulate: Object encapsulation & mapping system with runtime kernel for TypeScript
4
4
 
5
5
  Copyright 2026 Christoph Dorn - https://Christoph.diy
6
6
 
package/README.md CHANGED
@@ -1,8 +1,12 @@
1
- ⚠️ **WARNING:** This repository may get squashed and force-pushed if the [GordianOpenIntegrity](https://github.com/Stream44/t44-blockchaincommons.com) implementation must change in incompatible ways. Keep your diffs until the **GordianOpenIntegrity** system is stable.
2
-
3
- 🔷 **Open Development Project:** The implementation is a preview release for community feedback.
1
+ <table>
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>
5
+ <td>Preview release for community feedback.<br/>Get in touch on <a href="https://discord.gg/9eBcQXEJAN">discord</a>.</td>
6
+ </tr>
7
+ </table>
4
8
 
5
- ⚠️ **Disclaimer:** Under active development. Code has not been audited, APIs and interfaces are subject to change.
9
+ ⚠️ **Disclaimer:** Under active development. Code has not been audited. APIs and interfaces are subject to change!
6
10
 
7
11
  encapsulate [![Tests](https://github.com/Stream44/encapsulate/actions/workflows/test.yaml/badge.svg)](https://github.com/Stream44/encapsulate/actions/workflows/test.yaml?query=branch%3Amain)
8
12
  ===
@@ -21,17 +25,24 @@ It is being used to underpin:
21
25
  <br/><br/>
22
26
  </p>
23
27
 
24
- Notes
28
+ The CAPSULE Spine Contract
25
29
  ---
26
30
 
27
- - The first spine contract is defined and implemented here: [src/spine-contracts/CapsuleSpineContract.v0/](src/spine-contracts/CapsuleSpineContract.v0/)
28
- - Projector reference implementations are here: [github.com/Stream44/ink-component-projector](https://github.com/Stream44/ink-component-projector)
31
+ The `encapsulate` library wraps TypeScript objects and binds reference trees for constructing executable component graphs.
29
32
 
30
- Roadmap
31
- ---
33
+ The binding rules are defined by **Spine Contracts**. The first *experimental* spine contract is the **Capsule Spine Contract**. It builds a model
34
+ around **Capsules** which have certain properties.
35
+
36
+ The capsule spine contract is implemented here: [src/spine-contracts/CapsuleSpineContract.v0/](src/spine-contracts/CapsuleSpineContract.v0/)
37
+
38
+ ### Roadmap
39
+
40
+ - [ ] Private/Projected properties
41
+ - [ ] Property annotations
42
+ - [ ] Capsule Projectors
43
+ - [ ] Load capsules from packs
32
44
 
33
- - [ ] Document [src/spine-contracts/CapsuleSpineContract.v0/](src/spine-contracts/CapsuleSpineContract.v0/)
34
- - [ ] Private properties
45
+ ![Capsule Spine Contract Overview](./src/spine-contracts/CapsuleSpineContract.v0/Overview.svg)
35
46
 
36
47
 
37
48
  Provenance
@@ -51,4 +62,4 @@ Repository DID: `did:repo:65bf6c297919ca938c513cdb7517605d0d62cdbf`
51
62
  </tr>
52
63
  </table>
53
64
 
54
- (c) 2026 [Christoph.diy](https://christoph.diy) • Code: `MIT` • Text: [GNU Free Documentation License](https://www.gnu.org/licenses/fdl-1.3.txt) • Created with [Stream44.Studio](https://Stream44.Studio)
65
+ (c) 2026 [Christoph.diy](https://christoph.diy) • Code: [MIT](./LICENSE.txt) • Text: [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) • Created with [Stream44.Studio](https://Stream44.Studio)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stream44.studio/encapsulate",
3
- "version": "0.4.0-rc.19",
3
+ "version": "0.4.0-rc.21",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -116,8 +116,8 @@ export function CapsuleModuleProjector({
116
116
  // Helper: Find custom projection path from property names starting with '/'
117
117
  function findCustomProjectionPath(capsule: any, spineContractUri: string): string | null {
118
118
  const spineContract = capsule.cst.spineContracts[spineContractUri]
119
- if (spineContract?.properties) {
120
- for (const propName in spineContract.properties) {
119
+ if (spineContract?.propertyContracts) {
120
+ for (const propName in spineContract.propertyContracts) {
121
121
  if (propName.startsWith('/')) {
122
122
  return propName.substring(1) // Remove leading '/'
123
123
  }
@@ -131,10 +131,10 @@ export function CapsuleModuleProjector({
131
131
  const mapped: Array<{ capsuleHash: string, projectionPath: string, capsule: any }> = []
132
132
  const spineContract = capsule.cst.spineContracts[spineContractUri]
133
133
 
134
- if (spineContract?.properties) {
135
- for (const propName in spineContract.properties) {
134
+ if (spineContract?.propertyContracts) {
135
+ for (const propName in spineContract.propertyContracts) {
136
136
  if (propName.startsWith('/')) {
137
- const prop = spineContract.properties[propName]
137
+ const prop = spineContract.propertyContracts[propName]
138
138
  // Check if this is a Mapping type property
139
139
  if (prop.type === 'CapsulePropertyTypes.Mapping') {
140
140
  // First try to find the mapped capsule in ambient references
@@ -178,7 +178,7 @@ export function CapsuleModuleProjector({
178
178
  }
179
179
  // Also check nested property contracts that start with '#'
180
180
  if (propName.startsWith('#')) {
181
- const propContract = spineContract.properties[propName] as any
181
+ const propContract = spineContract.propertyContracts[propName] as any
182
182
  if (propContract?.properties) {
183
183
  for (const nestedPropName in propContract.properties) {
184
184
  if (nestedPropName.startsWith('/')) {
@@ -264,8 +264,8 @@ export function CapsuleModuleProjector({
264
264
  }
265
265
 
266
266
  const spineContract = capsule.cst.spineContracts[spineContractUri]
267
- if (spineContract?.properties) {
268
- traverseProperties(spineContract.properties)
267
+ if (spineContract?.propertyContracts) {
268
+ traverseProperties(spineContract.propertyContracts)
269
269
  }
270
270
 
271
271
  return uris
@@ -275,8 +275,8 @@ export function CapsuleModuleProjector({
275
275
  function hasSolidJsProperty(capsule: any, spineContractUri: string): boolean {
276
276
  const spineContract = capsule.cst.spineContracts[spineContractUri]
277
277
  // Check both top-level and nested under '#' property contract
278
- const topLevelProps = spineContract?.properties || {}
279
- const nestedProps = spineContract?.properties?.['#']?.properties || {}
278
+ const topLevelProps = spineContract?.propertyContracts || {}
279
+ const nestedProps = spineContract?.propertyContracts?.['#']?.properties || {}
280
280
 
281
281
  // Check for solidjs.com/standalone specifically
282
282
  for (const key of Object.keys(topLevelProps)) {
@@ -292,8 +292,8 @@ export function CapsuleModuleProjector({
292
292
  function hasStandaloneProperty(capsule: any, spineContractUri: string): boolean {
293
293
  const spineContract = capsule.cst.spineContracts[spineContractUri]
294
294
  // Check both top-level and nested under '#' property contract
295
- const topLevelProps = spineContract?.properties || {}
296
- const nestedProps = spineContract?.properties?.['#']?.properties || {}
295
+ const topLevelProps = spineContract?.propertyContracts || {}
296
+ const nestedProps = spineContract?.propertyContracts?.['#']?.properties || {}
297
297
 
298
298
  // Check for exact match or with suffix
299
299
  for (const key of Object.keys(topLevelProps)) {
@@ -310,8 +310,8 @@ export function CapsuleModuleProjector({
310
310
  const spineContract = capsule.cst.spineContracts[spineContractUri]
311
311
 
312
312
  // Check nested under '#' property contract first, looking for solidjs.com/standalone
313
- const nestedProps = spineContract?.properties?.['#']?.properties || {}
314
- const topLevelProps = spineContract?.properties || {}
313
+ const nestedProps = spineContract?.propertyContracts?.['#']?.properties || {}
314
+ const topLevelProps = spineContract?.propertyContracts || {}
315
315
 
316
316
  let solidjsProp = null
317
317
  for (const key of Object.keys(nestedProps)) {
@@ -370,8 +370,8 @@ export function CapsuleModuleProjector({
370
370
  const spineContract = capsule.cst.spineContracts[spineContractUri]
371
371
 
372
372
  // Check nested under '#' property contract first, looking for encapsulate.dev/standalone or encapsulate.dev/standalone/*
373
- const nestedProps = spineContract?.properties?.['#']?.properties || {}
374
- const topLevelProps = spineContract?.properties || {}
373
+ const nestedProps = spineContract?.propertyContracts?.['#']?.properties || {}
374
+ const topLevelProps = spineContract?.propertyContracts || {}
375
375
 
376
376
  let standaloneProp = null
377
377
  for (const key of Object.keys(nestedProps)) {
@@ -503,7 +503,7 @@ export function CapsuleModuleProjector({
503
503
 
504
504
  // Only project capsules that have the Capsule struct property
505
505
  const spineContract = capsule.cst.spineContracts[spineContractUri]
506
- if (!spineContract?.properties?.['#@stream44.studio/encapsulate/structs/Capsule']) {
506
+ if (!spineContract?.propertyContracts?.['#@stream44.studio/encapsulate/structs/Capsule']) {
507
507
  return false
508
508
  }
509
509
 
@@ -571,11 +571,11 @@ export function CapsuleModuleProjector({
571
571
  if (potentialMappedCapsule === capsule) continue
572
572
 
573
573
  // Check if this capsule has the Capsule struct (meaning it should be projected)
574
- if (potentialMappedCapsule.cst?.spineContracts?.[spineContractUri]?.properties?.['#@stream44.studio/encapsulate/structs/Capsule']) {
574
+ if (potentialMappedCapsule.cst?.spineContracts?.[spineContractUri]?.propertyContracts?.['#@stream44.studio/encapsulate/structs/Capsule']) {
575
575
  // Check if this capsule's moduleFilepath is referenced in any mapping property
576
576
  const mappedModulePath = potentialMappedCapsule.cst.source.moduleFilepath
577
577
 
578
- for (const [propContractKey, propContract] of Object.entries(spineContract.properties)) {
578
+ for (const [propContractKey, propContract] of Object.entries(spineContract.propertyContracts)) {
579
579
  if (propContractKey.startsWith('#') && (propContract as any).properties) {
580
580
  for (const [propName, propDef] of Object.entries((propContract as any).properties)) {
581
581
  const prop = propDef as any
@@ -1,7 +1,7 @@
1
1
 
2
2
  // CACHE_BUST_VERSION: Increment this whenever CST cache must be invalidated due to structural changes
3
3
  // This ensures projected capsules are regenerated when the CST format changes
4
- const CACHE_BUST_VERSION = 11
4
+ const CACHE_BUST_VERSION = 18
5
5
 
6
6
  type TSpineOptions = {
7
7
  spineFilesystemRoot?: string,
@@ -522,7 +522,8 @@ async function encapsulate(definition: TCapsuleDefinition, options: TCapsuleOpti
522
522
  }
523
523
 
524
524
  // Get capsuleName from options first, then fall back to CST if available
525
- const cst = csts?.[capsuleSourceLineRef]
525
+ // When parseModule returns CSTs without this capsule (e.g. projected files), fall back to options.cst
526
+ const cst = csts?.[capsuleSourceLineRef] || options.cst
526
527
  const capsuleName = options.capsuleName || cst?.source?.capsuleName
527
528
 
528
529
  const encapsulateOptions: TEncapsulateOptions = {
@@ -613,9 +614,13 @@ async function encapsulate(definition: TCapsuleDefinition, options: TCapsuleOpti
613
614
  propertyContractDefinitions[spineContractUri]['#'] = {}
614
615
  }
615
616
 
617
+ // Look up capsule object from spine registry if available (for inline capsule refs)
618
+ const delegateUri = propContractUri.substring(1)
619
+ const delegateCapsuleObj = spine.capsules[delegateUri]
620
+
616
621
  propertyContractDefinitions[spineContractUri]['#'][contractKey] = {
617
622
  type: CapsulePropertyTypes.Mapping,
618
- value: propContractUri.substring(1),
623
+ value: delegateCapsuleObj || delegateUri,
619
624
  propertyContractDelegate: propContractUri,
620
625
  as: aliasName,
621
626
  // Pass options from the property contract delegate to the mapped capsule
@@ -711,13 +716,12 @@ async function encapsulate(definition: TCapsuleDefinition, options: TCapsuleOpti
711
716
  // The selfProxy in spine contracts will expose this as 'self' property
712
717
  const ownSelf = merge({}, defaultInstance, defaultPropertyValues, ...Object.values(mergedValuesByContract))
713
718
 
714
- // Capsule metadata struct will be set on self/ownSelf AFTER spine contract processing
715
- // to avoid being overwritten by the empty struct marker in the definition
716
719
  // Convert relative paths to absolute for metadata exposure
717
720
  const absoluteCapsuleSourceLineRef = `${absoluteModuleFilepath}:${importStackLine}`
718
- const capsuleMetadataStruct = {
721
+ const capsuleMetadataStruct: Record<string, any> = {
719
722
  capsuleName: encapsulateOptions.capsuleName,
720
723
  capsuleSourceLineRef: absoluteCapsuleSourceLineRef,
724
+ capsuleSourceNameRefHash: cst?.capsuleSourceNameRefHash,
721
725
  moduleFilepath: absoluteModuleFilepath,
722
726
  // Root capsule metadata will be populated after extends chain is resolved
723
727
  rootCapsule: {
@@ -734,23 +738,34 @@ async function encapsulate(definition: TCapsuleDefinition, options: TCapsuleOpti
734
738
  // Check CST first, then fall back to encapsulateOptions for direct capsule references
735
739
  let extendsCapsuleValue = capsule.cst?.source?.extendsCapsule || encapsulateOptions.extendsCapsule
736
740
 
737
- // If extendsCapsule is a string identifier, check if it's in ambientReferences first
741
+ // If extendsCapsule is a string identifier, resolve from ambient references
738
742
  if (typeof extendsCapsuleValue === 'string') {
739
743
  const cstAmbientRefs = capsule.cst?.source?.ambientReferences || {}
740
744
  const runtimeAmbientRefs = encapsulateOptions.ambientReferences || {}
741
- for (const [refName, ref] of Object.entries(cstAmbientRefs)) {
742
- const refTyped = ref as any
743
- if (refName === extendsCapsuleValue) {
744
- if (refTyped.type === 'capsule' && refTyped.value) {
745
- extendsCapsuleValue = refTyped.value
746
- } else if (refTyped.type === 'instance' && runtimeAmbientRefs[refName]) {
747
- // CST stores '[instance]' placeholder; resolve from runtime ambient refs
748
- const runtimeRef = runtimeAmbientRefs[refName]
749
- if (runtimeRef && typeof runtimeRef === 'object' && typeof runtimeRef.makeInstance === 'function') {
750
- extendsCapsuleValue = runtimeRef
745
+
746
+ // First: try runtime ambient refs directly (capsule objects with .makeInstance)
747
+ if (runtimeAmbientRefs[extendsCapsuleValue]) {
748
+ const runtimeRef = runtimeAmbientRefs[extendsCapsuleValue]
749
+ if (runtimeRef && typeof runtimeRef === 'object' && typeof runtimeRef.makeInstance === 'function') {
750
+ extendsCapsuleValue = runtimeRef
751
+ }
752
+ }
753
+
754
+ // Second: try CST ambient refs if still a string
755
+ if (typeof extendsCapsuleValue === 'string') {
756
+ for (const [refName, ref] of Object.entries(cstAmbientRefs)) {
757
+ const refTyped = ref as any
758
+ if (refName === extendsCapsuleValue) {
759
+ if (refTyped.type === 'capsule' && refTyped.value) {
760
+ extendsCapsuleValue = refTyped.value
761
+ } else if (refTyped.type === 'instance' && runtimeAmbientRefs[refName]) {
762
+ const runtimeRef = runtimeAmbientRefs[refName]
763
+ if (runtimeRef && typeof runtimeRef === 'object' && typeof runtimeRef.makeInstance === 'function') {
764
+ extendsCapsuleValue = runtimeRef
765
+ }
751
766
  }
767
+ break
752
768
  }
753
- break
754
769
  }
755
770
  }
756
771
  }
@@ -824,6 +839,14 @@ async function encapsulate(definition: TCapsuleDefinition, options: TCapsuleOpti
824
839
  rootCapsule: resolvedRootCapsule
825
840
  }
826
841
 
842
+ // Set capsule metadata struct on self early so it's available in options() callbacks during mapping
843
+ if (!self['#@stream44.studio/encapsulate/structs/Capsule'] ||
844
+ typeof self['#@stream44.studio/encapsulate/structs/Capsule'] !== 'object' ||
845
+ !self['#@stream44.studio/encapsulate/structs/Capsule'].capsuleName) {
846
+ self['#@stream44.studio/encapsulate/structs/Capsule'] = capsuleMetadataStruct
847
+ }
848
+ ownSelf['#@stream44.studio/encapsulate/structs/Capsule'] = capsuleMetadataStruct
849
+
827
850
  // Use runtime spine contracts if provided, otherwise fall back to encapsulation spine contracts
828
851
  const activeSpineContracts = runtimeSpineContracts || spine.spineContracts
829
852
 
@@ -853,10 +876,18 @@ async function encapsulate(definition: TCapsuleDefinition, options: TCapsuleOpti
853
876
  if (propertyContractUri !== '#') {
854
877
  continue
855
878
  }
879
+ // Get CST property definitions for this spine contract to merge depends
880
+ const cstProperties = cst?.spineContracts?.[spineContractUri]?.propertyContracts?.[propertyContractUri]?.properties
881
+
856
882
  for (const [propertyName, propertyDefinition] of Object.entries(properties)) {
857
883
 
858
884
  if (!propertyDefinition.type || !(propertyDefinition.type in CapsulePropertyTypes)) throw new Error(`Type '${propertyDefinition.type}' for property '${propertyName}' on spineContract '${spineContractUri}' not set or supported!`)
859
885
 
886
+ // Merge CST depends into property definition (CST is authoritative)
887
+ const cstDepends = cstProperties?.[propertyName]?.depends
888
+ if (cstDepends && !propertyDefinition.depends) {
889
+ propertyDefinition.depends = cstDepends
890
+ }
860
891
  await spineContractCapsuleInstance.mapProperty({
861
892
  overrides,
862
893
  options,
@@ -866,17 +897,17 @@ async function encapsulate(definition: TCapsuleDefinition, options: TCapsuleOpti
866
897
  propertyContractUri
867
898
  }
868
899
  })
900
+
901
+ // Re-set capsule metadata after the Capsule struct delegate mapping overwrites self
902
+ if (propertyDefinition.propertyContractDelegate === '#@stream44.studio/encapsulate/structs/Capsule') {
903
+ self[propertyName] = capsuleMetadataStruct
904
+ }
869
905
  }
870
906
  }
871
907
  }
872
908
 
873
- // Set capsule metadata struct on self/ownSelf AFTER spine contract processing
874
- // to avoid being overwritten by the empty struct marker in the definition
875
- if (!self['#@stream44.studio/encapsulate/structs/Capsule'] ||
876
- typeof self['#@stream44.studio/encapsulate/structs/Capsule'] !== 'object' ||
877
- !self['#@stream44.studio/encapsulate/structs/Capsule'].capsuleName) {
878
- self['#@stream44.studio/encapsulate/structs/Capsule'] = capsuleMetadataStruct
879
- }
909
+ // Ensure capsule metadata struct is set on ownSelf after spine contract processing
910
+ // (self may have been updated by the extends chain; ownSelf always reflects this capsule)
880
911
  ownSelf['#@stream44.studio/encapsulate/structs/Capsule'] = capsuleMetadataStruct
881
912
 
882
913
  // Collect lifecycle functions and mapped capsule instances from all spine contract capsule instances
@@ -127,10 +127,12 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
127
127
 
128
128
  // delegateOptions is set by encapsulate.ts for property contract delegates
129
129
  // options can be a function or an object for regular mappings
130
+ // Always pass { self, constants } - self is populated when depends is specified, empty otherwise
131
+ const optionsFn = property.definition.options
130
132
  const mappingOptions = property.definition.delegateOptions
131
- || (typeof property.definition.options === 'function'
132
- ? await property.definition.options({ constants })
133
- : property.definition.options)
133
+ || (typeof optionsFn === 'function'
134
+ ? await optionsFn({ self: property.definition.depends ? this.self : {}, constants })
135
+ : optionsFn)
134
136
 
135
137
  // Check for existing instance in registry - reuse if available (regardless of options)
136
138
  // Pre-registration with null allows parent capsules to "claim" a slot before child capsules process
@@ -333,7 +335,7 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
333
335
  // This avoids triggering the proxy and firing unwanted membrane events
334
336
  const delegateTarget = this.encapsulatedApi[property.definition.propertyContractDelegate]
335
337
  const mappedCapsuleCst = mappedCapsule.cst
336
- const spineContractProperties = mappedCapsuleCst?.spineContracts?.[this.spineContractUri]?.properties
338
+ const spineContractProperties = mappedCapsuleCst?.spineContracts?.[this.spineContractUri]?.propertyContracts
337
339
 
338
340
  if (spineContractProperties) {
339
341
  for (const [key, propDef] of Object.entries(spineContractProperties)) {