en un clic
add-context
Create a new business domain context
Installer avec Codex ou Claude Copiez ce prompt, collez-le dans Codex, Claude ou un autre assistant, puis laissez-le vérifier la page du skill et l'installer pour vous.
Menu
Create a new business domain context
Installer avec Codex ou Claude Copiez ce prompt, collez-le dans Codex, Claude ou un autre assistant, puis laissez-le vérifier la page du skill et l'installer pour vous.
Basé sur la classification professionnelle SOC
| name | add-context |
| description | Create a new business domain context |
Guide for creating a new context following clean architecture with Domain-Driven Design.
Ask the user for:
Contexts/Popcorn{Context}/
├── Package.swift
├── Sources/
│ ├── {Context}Domain/ # Layer 1: Pure business logic
│ │ ├── Entities/
│ │ ├── Repositories/
│ │ └── DataSources/
│ ├── {Context}Application/ # Layer 2: Use cases
│ │ ├── UseCases/
│ │ └── {Context}ApplicationFactory.swift
│ ├── {Context}Infrastructure/ # Layer 3: Data sources
│ │ ├── DataSources/
│ │ │ ├── Local/
│ │ │ │ ├── Models/
│ │ │ │ └── Mappers/
│ │ │ └── Protocols/
│ │ ├── Repositories/
│ │ └── {Context}InfrastructureFactory.swift
│ └── {Context}Composition/ # Layer 4: Wiring
│ └── Popcorn{Context}Factory.swift
└── Tests/
└── {Context}ApplicationTests/
// swift-tools-version: 6.2
import PackageDescription
let package = Package(
name: "Popcorn{Context}",
platforms: [
.iOS(.v26),
.macOS(.v26),
.visionOS(.v26)
],
products: [
.library(name: "{Context}Domain", targets: ["{Context}Domain"]),
.library(name: "{Context}Application", targets: ["{Context}Application"]),
.library(name: "{Context}Infrastructure", targets: ["{Context}Infrastructure"]),
.library(name: "{Context}Composition", targets: ["{Context}Composition"])
],
dependencies: [
.package(path: "../../Core/CoreDomain")
],
targets: [
// Domain - NO dependencies (pure business logic)
.target(name: "{Context}Domain"),
// Application - depends on Domain only
.target(
name: "{Context}Application",
dependencies: ["{Context}Domain"]
),
// Infrastructure - depends on Domain only
.target(
name: "{Context}Infrastructure",
dependencies: ["{Context}Domain"]
),
// Composition - depends on all above
.target(
name: "{Context}Composition",
dependencies: [
"{Context}Domain",
"{Context}Application",
"{Context}Infrastructure"
]
),
// Tests
.testTarget(
name: "{Context}ApplicationTests",
dependencies: ["{Context}Application", "{Context}Domain"]
)
]
)
Entities/{Entity}.swift:
public struct {Entity}: Identifiable, Equatable, Sendable {
public let id: Int
public let name: String
// Add properties
public init(id: Int, name: String) {
self.id = id
self.name = name
}
}
Repositories/{Entity}Repository.swift:
public protocol {Entity}Repository: Sendable {
func {entity}(withID id: Int) async throws({Entity}RepositoryError) -> {Entity}
}
public enum {Entity}RepositoryError: Error, Equatable, Sendable {
case notFound
case unknown(Error? = nil)
}
Follow the add-use-case workflow for each use case.
{Context}ApplicationFactory.swift:
public final class {Context}ApplicationFactory: Sendable {
private let {entity}Repository: any {Entity}Repository
public init({entity}Repository: some {Entity}Repository) {
self.{entity}Repository = {entity}Repository
}
public func makeFetch{Entity}UseCase() -> some Fetch{Entity}UseCase {
DefaultFetch{Entity}UseCase({entity}Repository: {entity}Repository)
}
}
DataSources/Protocols/Remote/{Entity}RemoteDataSource.swift:
public protocol {Entity}RemoteDataSource: Sendable {
func {entity}(withID id: Int) async throws({Entity}RemoteDataSourceError) -> {Entity}
}
public enum {Entity}RemoteDataSourceError: Error {
case notFound
case unauthorised
case unknown(Error? = nil)
}
Repositories/Default{Entity}Repository.swift:
final class Default{Entity}Repository: {Entity}Repository {
private let remoteDataSource: any {Entity}RemoteDataSource
private let localDataSource: any {Entity}LocalDataSource
func {entity}(withID id: Int) async throws({Entity}RepositoryError) -> {Entity} {
// Cache-first strategy
}
}
Popcorn{Context}Factory.swift — a concrete Sendable final class (there is no
factory protocol; the concrete factory is the public API, like the adapter factories):
public final class Popcorn{Context}Factory: Sendable {
private let applicationFactory: {Context}ApplicationFactory
public init({entity}RemoteDataSource: some {Entity}RemoteDataSource, modelContainer: ModelContainer) {
let infrastructure = {Context}InfrastructureFactory(
remoteDataSource: {entity}RemoteDataSource,
modelContainer: modelContainer
)
self.applicationFactory = {Context}ApplicationFactory(
{entity}Repository: infrastructure.make{Entity}Repository()
)
}
public func makeFetch{Entity}UseCase() -> some Fetch{Entity}UseCase {
applicationFactory.makeFetch{Entity}UseCase()
}
}
The adapters package bridges external services (TMDb) to the context's ports —
the *RemoteDataSource and provider protocols defined in the Domain/Infrastructure
layers. Its factory exposes those adapters through make…() -> some Port methods.
It does not build Popcorn{Context}Factory, and it must not depend on
{Context}Composition. Assembling the context factory from these adapters is the
composition root's job (Step 8) — this keeps the adapters package a leaf in the
dependency graph rather than reaching across into the context's composition layer.
Adapters/Contexts/Popcorn{Context}Adapters/
├── Package.swift
├── Sources/
│ └── Popcorn{Context}Adapters/
│ ├── DataSources/
│ │ ├── TMDb{Entity}RemoteDataSource.swift # conforms to {Entity}RemoteDataSource (internal)
│ │ └── Mappers/ # TMDb DTO → Domain mappers
│ └── Popcorn{Context}AdaptersFactory.swift
└── Tests/
└── Popcorn{Context}AdaptersTests/
Package.swift — depend on the context's Domain and Infrastructure
products (where the ports live) plus TMDb. Do not add {Context}Composition:
dependencies: [
.product(name: "{Context}Domain", package: "Popcorn{Context}"),
.product(name: "{Context}Infrastructure", package: "Popcorn{Context}"),
"TMDb"
]
Popcorn{Context}AdaptersFactory.swift — return the port implementations, one
make…() per port. Keep the concrete adapter types internal; the some Port
return types hide them. Qualify the TMDb service type as TMDb.{Entity}Service to
avoid ambiguity once {Context}Infrastructure is imported:
import {Context}Infrastructure
import TMDb
public final class Popcorn{Context}AdaptersFactory {
private let {entity}Service: any TMDb.{Entity}Service
public init({entity}Service: some TMDb.{Entity}Service) {
self.{entity}Service = {entity}Service
}
public func make{Entity}RemoteDataSource() -> some {Entity}RemoteDataSource {
TMDb{Entity}RemoteDataSource({entity}Service: {entity}Service)
}
}
A provider adapter backed by another context's use case (e.g. an
AppConfigurationProviding) takes that use case in the factory'sinitand is exposed via its ownmake…()method. Concerns that are not adapters —themeColorProvider,observability, amodelContainer— do not belong on this factory; the composition root passes them straight into the context factory (Step 8).
AppServices (in AppDependencies/Sources/AppDependencies/Composition/) is the
app's composition root: it builds the shared service and factory graph once, in
dependency order. Add the new context factory there so features can reach its use
cases:
AppServices.swift — declare the factory as a stored property, and add a
matching field to the Graph struct + the init assignment:
public let {context}Factory: Popcorn{Context}Factory
// ...in Graph:
let {context}Factory: Popcorn{Context}Factory
// ...in init:
self.{context}Factory = graph.{context}Factory
AppServices+Composition.swift — in buildGraph, build the adapters factory
from the shared TMDb services, then assemble Popcorn{Context}Factory from the
adapters it produces (plus any non-adapter concerns like the model container or
theme-colour provider), and pass it into the returned Graph. The composition root
is the single place that assembles context factories from adapters:
let {context}Adapters = Popcorn{Context}AdaptersFactory(
{entity}Service: domain.tmdb.{entities}
)
let {context}Factory = Popcorn{Context}Factory(
{entity}RemoteDataSource: {context}Adapters.make{Entity}RemoteDataSource(),
modelContainer: modelContainer
)
A feature then consumes these use cases in its Dependencies.live(services:)
builder via services.{context}Factory.make{UseCaseName}UseCase() — see the
add-use-case and add-feature workflows. There is no *+TCA.swift file and no
per-use-case dependency registration.
Add the new package to the main Xcode project's package dependencies.
Add all new unit test targets to TestPlans/PopcornUnitTests.xctestplan so tests run as part of the test plan. Add an entry to the testTargets array for each test target:
{
"target" : {
"containerPath" : "container:Contexts\/Popcorn{Context}",
"identifier" : "{Context}ApplicationTests",
"name" : "{Context}ApplicationTests"
}
}
Repeat for each test target (e.g., {Context}DomainTests, {Context}InfrastructureTests). Do NOT add snapshot test targets here — those belong in PopcornSnapshotTests.xctestplan.
$ARGUMENTS
Build the project for testing
Build the project
Take the current plan all the way to a ready-to-merge pull request — review the plan (scaled to risk), implement it test-first, code-review and fix, security-review, verify the acceptance criteria, open the PR, and watch it green. Use after you have an approved plan (e.g. from plan mode) and want the rest of the feature pipeline run end-to-end. Invoking it is itself plan approval — it then runs autonomously to a single hard stop: ready-to-merge.
Run a specific test target or test class
Run all unit tests
Run snapshot tests