-
Notifications
You must be signed in to change notification settings - Fork 0
Module Model and Manifests
Every Forsetti module has two contracts that must stay aligned:
- A Swift contract: protocol conformance, descriptor, manifest property, lifecycle implementation, and registry factory.
- A manifest contract: JSON identity, version compatibility, requested capabilities, default role, and runtime requirements.
Activation succeeds only when both contracts describe the same module.
flowchart LR
Swift["Swift module type"] --> Descriptor["ModuleDescriptor"]
Swift --> ManifestProp["module.manifest"]
JSON["Bundled manifest JSON"] --> Loader["ManifestLoader"]
Loader --> Discovered["Discovered ModuleManifest"]
Registry["ModuleRegistry entryPoint"] --> Factory["Factory returns module"]
Discovered --> Manager["ModuleManager"]
Factory --> Manager
Descriptor --> Identity["Identity validation"]
ManifestProp --> Identity
Discovered --> Identity
Identity --> RuntimeReq["Runtime requirement validation"]
RuntimeReq --> Lifecycle["start(scoped context)"]
classDiagram
class ForsettiModule {
+descriptor: ModuleDescriptor
+manifest: ModuleManifest
+start(context)
+stop(context)
}
class ForsettiUIModule {
+uiContributions: UIContributions
}
class ForsettiAppModule
ForsettiModule <|-- ForsettiUIModule
ForsettiUIModule <|-- ForsettiAppModule
| Module Type | Protocol | Purpose | Runtime Boundary |
|---|---|---|---|
service |
ForsettiModule |
Background behavior, shared providers, sync, diagnostics, or domain services. | Cannot provide UI contributions. |
ui |
ForsettiUIModule |
Feature UI that contributes toolbar items, view injections, overlays, or theme declarations. | Must declare UI requirements when using manifest template 1.1. |
app |
ForsettiAppModule |
Complete single-application module that carries the primary application UI. | Same UI declaration rules as ui, with app-level presentation intent. |
Schema 1.1 with manifest template 1.1 is the current authoring target. Schema 1.0 and template 1.0 remain supported for safe legacy manifests, but new modules should use the current shape.
{
"schemaVersion": "1.1",
"manifestTemplateVersion": "1.1",
"moduleID": "com.example.product.module",
"displayName": "Example Module",
"moduleVersion": {
"major": 1,
"minor": 0,
"patch": 0
},
"moduleType": "ui",
"supportedPlatforms": ["iOS", "macOS"],
"minForsettiVersion": {
"major": 0,
"minor": 1,
"patch": 1
},
"maxForsettiVersion": null,
"capabilitiesRequested": [
"toolbar_items",
"view_injection",
"routing_overlay",
"telemetry"
],
"iapProductID": null,
"entryPoint": "ExampleModule",
"defaultModuleRole": "ui",
"runtimeRequirements": {
"io": [
{
"requirementID": "telemetry-events",
"kind": "telemetry",
"access": "emit",
"required": true,
"description": "Publishes module lifecycle telemetry through the host telemetry service."
}
],
"ui": {
"controlSchemeID": "example.controls.primary",
"layoutID": "example.layout.default",
"themeIDs": ["example.theme.default"],
"viewIDs": ["example.primary"],
"slotIDs": ["main"],
"toolbarItemIDs": ["example.refresh"],
"routeIDs": ["example.detail"],
"pointerIDs": ["example.home"]
},
"dataIsolation": {
"mode": "private_to_module",
"ownedStoreIDs": ["example.private"],
"requiredDefaultRoles": []
}
}
}| Field | Required | Notes |
|---|---|---|
schemaVersion |
Yes | Supported values are 1.0 and 1.1; new manifests should use 1.1. |
manifestTemplateVersion |
Yes for current templates | Supported values are 1.0 and 1.1; schema 1.1 requires template 1.1. |
moduleID |
Yes | Stable unique identifier. Duplicate IDs fail discovery. |
displayName |
Yes | Human-readable module name. |
moduleVersion |
Yes | Semantic version object with major, minor, and patch. |
moduleType |
Yes |
service, ui, or app. |
supportedPlatforms |
Yes | One or more of iOS and macOS. |
minForsettiVersion |
Yes | Minimum framework runtime version required by this module. |
maxForsettiVersion |
No | Optional upper bound for compatibility. |
capabilitiesRequested |
Yes | Permission-like capability declarations evaluated before activation. |
iapProductID |
No | Store or product entitlement ID for paid or gated modules. |
entryPoint |
Yes | Registry key used to create the module instance. |
defaultModuleRole |
No | Declares that this module provides a default role such as ui, shared_database, authentication, diagnostics, api, or security. |
runtimeRequirements |
No for legacy, expected for current modules | Declares I/O, UI, and data isolation requirements. Missing values decode to safe legacy defaults. |
flowchart TD
Req["runtimeRequirements"] --> IO["io[]"]
Req --> UI["ui"]
Req --> Data["dataIsolation"]
IO --> Cap["Each I/O kind requires matching capability"]
IO --> Provider["Required entries require an available provider"]
UI --> Decl["Contributed IDs must be declared"]
Data --> Mode["private_to_module or framework_mediated_shared"]
Data --> Roles["requiredDefaultRoles require providers"]
| Field | Purpose |
|---|---|
requirementID |
Stable local ID for diagnostics and review. |
kind |
Service family: networking, storage, secure_storage, file_export, telemetry, shared_database, authentication, diagnostics, api, or security. |
access |
Intended use: read, write, read_write, execute, emit, or consume. |
required |
true means activation fails when the matching provider is unavailable. |
description |
Review note describing why the module needs this surface. |
Every I/O kind maps to a capability. A manifest that declares an I/O requirement without the matching capability fails activation with unsatisfiedRuntimeRequirement.
Manifest template 1.1 requires UI/app modules to declare the UI IDs they will contribute:
| UI Declaration | Checked Against |
|---|---|
themeIDs |
ThemeMask.themeID before sanitization. |
viewIDs |
ViewInjectionDescriptor.viewID. |
slotIDs |
ViewInjectionDescriptor.slot. |
toolbarItemIDs |
ToolbarItemDescriptor.itemID. |
routeIDs |
OverlayRoute.routeID and ToolbarAction.openOverlay. |
pointerIDs |
NavigationPointer.pointerID and ToolbarAction.navigate. |
The runtime validates declarations before applying UI contributions. Theme masks are currently validated when present, then stripped before publication to the default UISurfaceManager because framework shell theming remains host-controlled.
| Mode | Meaning |
|---|---|
private_to_module |
Module data stays module-scoped by default. |
framework_mediated_shared |
Module expects framework-mediated shared data access and requires an available shared_database provider. |
requiredDefaultRoles lists default-role providers that must be present among discovered manifests before activation can proceed.
| Capability | Raw Value | Typical Use |
|---|---|---|
networking |
networking |
Network requests through a host-provided networking service. |
storage |
storage |
Non-sensitive storage. |
secureStorage |
secure_storage |
Sensitive data storage. |
fileExport |
file_export |
Export files through a host-approved destination. |
cryptoUtilities |
crypto_utilities |
Cryptographic helper access. |
telemetry |
telemetry |
Event tracking through the host telemetry service. |
routingOverlay |
routing_overlay |
Overlay routes and navigation pointers. |
uiThemeMask |
ui_theme_mask |
Theme-mask declarations; default publication is still host-controlled. |
toolbarItems |
toolbar_items |
Toolbar item contributions. |
viewInjection |
view_injection |
Slot-based SwiftUI view injection. |
sharedDatabase |
shared_database |
Shared data provider access. |
authentication |
authentication |
Authentication provider access. |
diagnostics |
diagnostics |
Diagnostics provider access. |
api |
api |
API provider access. |
security |
security |
Security provider access. |
flowchart LR
Role["defaultModuleRole"] --> Type{"Valid for moduleType?"}
Type -- no --> Block["unsatisfiedRuntimeRequirement"]
Type -- yes --> Capability{"Role requires capability?"}
Capability -- yes --> CapCheck["Capability must be requested"]
Capability -- no --> OK["Role accepted"]
CapCheck --> Provider["Other modules can depend on role availability"]
| Role | Valid Module Type | Required Capability |
|---|---|---|
ui |
ui, app
|
none |
shared_database |
service |
shared_database |
authentication |
service |
authentication |
diagnostics |
service |
diagnostics |
api |
service |
api |
security |
service |
security |
Default roles let service modules act as named providers for other modules without hard-coding concrete implementations into consumers.
let registry = ModuleRegistry()
registry.register(entryPoint: ExampleModule.Constants.entryPoint) {
ExampleModule()
}The manifest entryPoint must match the registry key. A discovered manifest without a matching registry factory cannot activate.
When a module activates, Forsetti creates a ModuleRegistrationRecord from the manifest and stores it in the configured ModuleRegistrationStore.
sequenceDiagram
participant Manager as ModuleManager
participant Manifest as ModuleManifest
participant Record as ModuleRegistrationRecord
participant Store as ModuleRegistrationStore
Manager->>Manifest: activation succeeded
Manager->>Record: make(manifest)
Record-->>Manager: sorted-key manifest hash and contract fields
Manager->>Store: saveRecord(record)
Store-->>Manager: persisted registration
The record includes identity, schema, manifest template, manifest hash, platforms, capabilities, default role, runtime requirements, and registration time. On later activations, the record helps detect drift between the previously accepted contract and the current manifest.
flowchart TD
A["Choose module type and role"] --> B["Create Swift module class"]
B --> C["Define descriptor and manifest property"]
C --> D["Create schema 1.1 manifest JSON"]
D --> E["Declare capabilities and runtimeRequirements"]
E --> F["Register entryPoint in ModuleRegistry"]
F --> G["Bundle manifest under Resources/ForsettiManifests"]
G --> H["Register host services and injected views"]
H --> I["Run tests and guardrails"]
flowchart TD
JSON["JSON file"] --> Shape{"Manifest root keys present?"}
Shape -- no in fallback mode --> Skip["Ignore file"]
Shape -- yes --> Decode["Decode ModuleManifest"]
Decode --> Schema{"Supported schema/template?"}
Schema -- no --> FailSchema["validationFailed"]
Schema -- yes --> Duplicate{"moduleID already loaded?"}
Duplicate -- yes --> Dup["duplicateModuleID"]
Duplicate -- no --> Compatibility["CompatibilityChecker"]
Compatibility --> Registry["Resolve entryPoint"]
Registry --> Identity["Descriptor and manifest identity validation"]
Identity --> Runtime["Runtime requirement validation"]
Runtime --> Lifecycle["start(scoped context)"]
Strict directory loading fails invalid JSON immediately. Fallback discovery ignores JSON files that do not contain the manifest root keys.
- Use schema
1.1and manifest template1.1for new modules. - Keep
moduleID,moduleType,moduleVersion,displayName, andentryPointidentical across descriptor, manifest property, JSON, and registry. - Request only capabilities the module actually uses.
- Add
runtimeRequirements.iofor every service surface the module needs. - Add
runtimeRequirements.uifor every contributed view, slot, toolbar item, route, pointer, and theme ID. - Use
defaultModuleRoleonly when the module really provides that role. - Keep service providers in host or service modules, not in reusable UI modules.
- Add focused tests for discovery, compatibility, activation, capability denial, and UI contribution validation.