This repository has no description
Swift 100%

README.md

Composable Workspace #

Composable Workspace is a small TCA26 foundation for applications that authenticate a person, authorize editing, maintain a local reference copy, and publish edits through proposals.

Warning

This package is under active development. Its core workspace lifecycle is exercised by a complete test suite and demo application, but its public API may still change as it is adopted by more applications.

Installation #

Add Composable Workspace to a Swift package:

dependencies: [
    .package(
        url: "https://source.woody.fm/swift-composable-workspace",
        branch: "main"
    )
]

Then add the products that match the ownership boundaries your application needs:

.target(
    name: "EventApplication",
    dependencies: [
        .product(
            name: "ComposableAuthoringWorkspace",
            package: "swift-composable-workspace"
        )
    ]
)
Product Purpose
ComposableAuthoring Authoring admission and validated capability state
ComposableWorkspace Source preparation, exact baseline selection, and proposal publication
ComposableAuthoringWorkspace Composition of authoring admission with workspace lifecycle
WorkspaceDemoFeature In-memory integration and executable application stories

Composable Workspace requires Swift 6.1 and supports iOS 17 and macOS 14.

The package deliberately separates four owners:

Owner Knows Does not know
Application authentication Session, token, sign-in, sign-out, network dependencies Authoring policy or proposal state
Authoring Admission policy and the currently validated authoring capability Source copies, branches, or proposals
Workspace The editable source, exact branch baseline, and proposal publication Login UI or application-specific admission policy
AuthoringWorkspace The two transitions between Authoring and Workspace Any additional business state

AuthoringWorkspace is intentionally one real TCA26 feature layer. TCA26 needs a concrete state and action identity when the feature is scoped. The layer owns no duplicate state machine: a newly validated grant requests workspace preparation, and an invalidated grant interrupts authorized workspace work. Authoring posts those lifecycle events after it changes the validated grant; AuthoringWorkspace handles the events explicitly. The composition does not infer authorization transitions from a later feature remount.

Composition #

Features receive their clients from Dependencies at the application composition root. Call sites do not pass infrastructure into Workspace or AuthoringWorkspace:

Scope(\.editing, action: \.editing) {
    AuthoringWorkspace {
        EventViewer()
    }
}

The application installs its concrete clients once:

EventApplication()
    .dependency(authoringClient)
    .dependency(workspaceClient)

The application also owns the lifecycle connections that are specific to it:

  • When the application starts, send restoreAuthorization so persisted authorization is revalidated. Incoming shared grant storage is cleared before wrapped content can observe it.
  • When the session identity changes, send authorizationInvalidated and await its returned task. If the new session is valid, then send refreshAdmission and await that task too. This prevents a late result from the previous identity from restoring stale admission or capability state.
  • Implement network clients using the current session/token and the AuthoringContext capability exposed to wrapped content.

Auth therefore remains an application feature. An OME application can compose its own Auth, AuthoringWorkspace, and EventViewer without this package knowing OME policy.

Workspace lifecycle #

The reference copy and writable content are different things. The writable content remains the editor's source of truth throughout publication.

The complete durable model is deliberately small:

struct WorkspaceState<Source: Sendable> {
  let id: WorkspaceID
  var availability: WorkspaceAvailability<Source>
}

enum WorkspaceAvailability<Source: Sendable> {
  case inactive
  case preparing
  case preparationFailed(WorkspaceFailure)
  case ready(WorkspaceSession<Source>)
}

struct WorkspaceSession<Source: Sendable> {
  var source: Source
  var baseline: WorkspaceBranchReference
}

ready always contains one coherent pair: the editable source and the exact branch revision against which its working edits are compared. Proposals are remote review records, not workspace modes and not mounted state.

prepareSource creates or opens that coherent session. It must not silently advance the installed baseline. refreshBaseline may update only the revision of the current branch. selectBranch explicitly installs another branch baseline. Refresh and selection are operations tracked by task IDs; they are not additional durable workspace states. A failed operation leaves the existing ready session intact.

Publication is one user operation:

  1. captureChanges compares writable content with the installed baseline and returns one in-memory ChangeSet.
  2. publishProposal creates one new remote proposal from that captured change set.
  3. Remote record creation is the publication commit point. Before that point, an error belongs to the caller's proposal form and the workspace remains in its existing ready session.
  4. After that point, projection lag must not turn publication into a failed workspace state. The integration may durably record a delivery receipt, but the workspace baseline does not change.

The ChangeSet generic is the captured difference understood by an integrator. It can be a database change manifest, a file patch, or a document snapshot. It is an ephemeral publication-attempt value, not another database and not durable workspace state.

Workspace state is scoped to workspaceID, not to the lifetime of an authoring grant. Losing a grant cancels active tasks but does not discard an already ready local session. Removing local workspace data is a separate operation owned by the integrating application.

For database-backed integrations, Source is the coherent database resource used for the entire editable workspace. It can contain the writable application database and a stable read-only reference reader. Descendants use that exact resource; queries must never locate or attach a reference database themselves, and applications must not reload a query to compensate for using a different writer.

Integration contract #

Every WorkspaceClient implementation must preserve these rules:

  • prepareSource returns a complete, coherent WorkspaceSession and never replaces writable content.
  • Workspace owns the returned source. Every later operation receives that exact value.
  • captureChanges captures against the session's installed baseline.
  • publishProposal is retry-safe and returns the exact proposal record identity, content revision, and target revision.
  • Once the remote record exists, publishProposal reports its identity without installing it into workspace state.
  • refreshBaseline preserves the selected branch identity.
  • selectBranch returns the exact requested branch only after it has been installed coherently.
  • Authorization is checked at every external operation. Throw WorkspaceAuthorizationError.invalidated when the current capability is no longer valid.
  • Throw WorkspaceHeadMismatch before creating a proposal record when its target has moved.

These are semantic requirements rather than assumptions hidden inside a Git, database, or network implementation, so different integrators can implement them in their own storage systems.

Failure model #

Preparation failure is workspace availability state because no coherent session exists yet. Capture or pre-record publication failure is reported to the proposal form while the ready session remains usable. Context refresh or selection failure is reported to the requesting UI and likewise preserves the session.

After remote record creation, delivery receipts belong to the integration's persistence layer. They bridge server-projection lag without inventing a user-visible "half published" workspace state.

Invalid actions and invalid client output are programmer or protocol errors. They are reported with withErrorReporting and do not become persistent UI state. Async operations mutate the store directly after returning; the package has no Result response actions.

Standalone proof #

The package exposes WorkspaceDemoFeature as the reusable application feature and in-memory demo system. The executable SwiftUI host lives in a separate Xcode application, so this package has no application target or duplicate UI surface. Open the companion application's WorkspaceDemo.xcworkspace and run the WorkspaceDemoApp scheme on an iPhone simulator.

The UI exercises sign-in, authoring entry, reference preparation, editing, proposal creation, creating independent proposals, refreshing Published after merge, and creating a fresh proposal. It intentionally does not inject synthetic backend failures.

Run the complete story and domain suite with:

swift test

The application stories mount makeWorkspaceDemoStore(), the same live feature graph and in-memory system as the Xcode host. They cover entry, the full create-create-merge-create path, and grant loss and re-entry without inventing proposal workspace state. Focused feature stories replace only external client operations to cover preparation, no-change publication, record failure, baseline selection, refresh, authorization loss, remote head movement, and invalid integrator output.