Skip to content

Module Model and Manifests

James Daley edited this page Jun 18, 2026 · 2 revisions

Module Model and Manifests

Every Forsetti module has two contracts that must stay aligned:

  1. A Swift contract: protocol conformance, descriptor, manifest property, lifecycle implementation, and registry factory.
  2. A manifest contract: JSON identity, version compatibility, requested capabilities, default role, and runtime requirements.

Activation succeeds only when both contracts describe the same module.

Contract Map

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)"]
Loading

Module Type Model

classDiagram
    class ForsettiModule {
      +descriptor: ModuleDescriptor
      +manifest: ModuleManifest
      +start(context)
      +stop(context)
    }
    class ForsettiUIModule {
      +uiContributions: UIContributions
    }
    class ForsettiAppModule
    ForsettiModule <|-- ForsettiUIModule
    ForsettiUIModule <|-- ForsettiAppModule
Loading
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.

Current Manifest Shape

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": []
    }
  }
}

Required Fields

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.

Runtime Requirements

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"]
Loading

I/O Requirements

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.

UI Requirements

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.

Data Isolation

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.

Capabilities

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.

Default Module Roles

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"]
Loading
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.

Registry Contract

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.

Registration Records

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
Loading

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.

Authoring Workflow

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"]
Loading

Validation Logic

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)"]
Loading

Strict directory loading fails invalid JSON immediately. Fallback discovery ignores JSON files that do not contain the manifest root keys.

Authoring Checklist

  • Use schema 1.1 and manifest template 1.1 for new modules.
  • Keep moduleID, moduleType, moduleVersion, displayName, and entryPoint identical across descriptor, manifest property, JSON, and registry.
  • Request only capabilities the module actually uses.
  • Add runtimeRequirements.io for every service surface the module needs.
  • Add runtimeRequirements.ui for every contributed view, slot, toolbar item, route, pointer, and theme ID.
  • Use defaultModuleRole only 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.

Clone this wiki locally