Add the Design Class Diagrams DCD-001 and DCD-002, align SD-001
- DCD-001 (UC-001): design classes refining DM-001, with class table, method traceability to OC-001 and SD-001, patterns and dependency check, and the mapping of each class to src/lib. - DCD-002: the consolidated project-level model, created from DCD-001. - SD-001: messages aligned with the DCD method signatures; cites DCD-001. - Dictionary, registry and traceability matrix updated. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
+312
@@ -0,0 +1,312 @@
|
||||
# Design Class Diagram
|
||||
|
||||
## Metadata
|
||||
| Key | Value |
|
||||
| --- | --- |
|
||||
| ID | DCD-002 |
|
||||
| CrossReference | [DCD-001], [DM-002], [UC-001], [OC-001], [SD-001], [DICT-001] |
|
||||
|
||||
## Version History
|
||||
| Date | Status | Author | Reviewer | Change | Commit |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| 2026-10-06 | Proposed | Jens Tirsvad Nielsen | S02 | Initial version, from DCD-001 (UC-001) | pending |
|
||||
|
||||
---
|
||||
|
||||
## Purpose and Scope
|
||||
|
||||
The consolidated design model of the project. Use-case models are scoped views; when one changes, this model is checked and updated in the same change. It currently covers [UC-001] "Create a new project" ([DCD-001]), which it was created from, and refines the concepts of [DM-002]. Class and attribute names are the IT terms of [DICT-001]. Every method traces to a contract in [OC-001] or a message in [SD-001].
|
||||
|
||||
The classes are design classes of a Bash program: a class is a group of functions in `src/lib/` with its data held in the shared state arrays (see "Implementation Mapping").
|
||||
|
||||
## Diagram
|
||||
|
||||
```plantuml
|
||||
@startuml
|
||||
skinparam classAttributeIconSize 0
|
||||
hide empty members
|
||||
|
||||
enum Visibility {
|
||||
private
|
||||
public
|
||||
}
|
||||
|
||||
class ProjectCreator <<controller>> {
|
||||
+startProjectCreation() : PromptSet
|
||||
+provideProjectDetails(name : String, description : String, visibility : Visibility, giteaOwner : Owner, githubOwner : Owner [0..1], directory : Path, enablePlanGate : Boolean) : Summary
|
||||
}
|
||||
class ConfigLoader {
|
||||
+load(configFile : Path, envFile : Path) : Configuration
|
||||
}
|
||||
class ToolChecker {
|
||||
+check(tools : String [1..*]) : ToolCheck
|
||||
}
|
||||
class Preflight {
|
||||
+check(request : ProjectRequest) : PreflightResult
|
||||
}
|
||||
abstract class GitHost <<facade>> {
|
||||
-name : String
|
||||
-webAddress : String
|
||||
-apiAddress : String
|
||||
+verifyToken() : Boolean
|
||||
+ownerAccepts(owner : Owner) : Boolean
|
||||
+nameFree(name : String) : Boolean
|
||||
}
|
||||
class GiteaClient <<facade>> {
|
||||
+GiteaClient(configuration : Configuration)
|
||||
+hasLicense(key : String) : Boolean
|
||||
+createRepository(request : ProjectRequest, license : String [0..1]) : GiteaRepository
|
||||
+addPushMirror(source : GiteaRepository, target : GitHubRepository) : PushMirror
|
||||
-requestSync(mirror : PushMirror) : void
|
||||
}
|
||||
class GitHubClient <<facade>> {
|
||||
+GitHubClient(configuration : Configuration)
|
||||
+createEmptyRepository(request : ProjectRequest) : GitHubRepository
|
||||
}
|
||||
class LocalProjectBuilder {
|
||||
+build(directory : Path, source : GiteaRepository, target : GitHubRepository [0..1], sshPassed : Boolean) : LocalProject
|
||||
}
|
||||
class FrameworkInstaller {
|
||||
+install(project : LocalProject, enablePlanGate : Boolean) : InstallResult
|
||||
}
|
||||
class SummaryReport {
|
||||
+compose(request : ProjectRequest) : Summary
|
||||
}
|
||||
|
||||
class Run {
|
||||
-isApply : Boolean
|
||||
}
|
||||
class Configuration {
|
||||
-giteaUrl : String
|
||||
-giteaApiUrl : String
|
||||
-githubWebUrl : String
|
||||
-githubApiUrl : String
|
||||
-giteaSshPort : Integer
|
||||
-mirrorInterval : String
|
||||
-frameworkRepo : String
|
||||
-presetDetails : Map [0..1]
|
||||
}
|
||||
class Credential {
|
||||
-kind : String
|
||||
-value : String
|
||||
}
|
||||
class ToolCheck {
|
||||
-hasGit : Boolean
|
||||
-hasCurl : Boolean
|
||||
-hasJq : Boolean
|
||||
}
|
||||
class PromptSet {
|
||||
-prompts : String [1..*]
|
||||
}
|
||||
class ProjectRequest {
|
||||
-name : String
|
||||
-description : String
|
||||
-visibility : Visibility
|
||||
-directory : Path
|
||||
-enablePlanGate : Boolean
|
||||
}
|
||||
class Owner {
|
||||
-name : String
|
||||
-kind : String
|
||||
}
|
||||
class PreflightResult {
|
||||
-tokensWork : Boolean
|
||||
-ownersAccept : Boolean
|
||||
-nameIsFree : Boolean
|
||||
-licenseIsOffered : Boolean
|
||||
-sshPassed : Boolean
|
||||
}
|
||||
abstract class Repository {
|
||||
-name : String
|
||||
-description : String
|
||||
-visibility : Visibility
|
||||
-address : String
|
||||
}
|
||||
class GiteaRepository
|
||||
class GitHubRepository
|
||||
class LicenseFile {
|
||||
-key : String
|
||||
}
|
||||
class PushMirror {
|
||||
-interval : String
|
||||
-syncOnCommit : Boolean
|
||||
}
|
||||
class LocalProject {
|
||||
-directory : Path
|
||||
}
|
||||
class Remote {
|
||||
-name : String
|
||||
-address : String
|
||||
}
|
||||
class Submodule {
|
||||
-name : String
|
||||
-address : String
|
||||
}
|
||||
class HookSetup {
|
||||
-areSkillsInstalled : Boolean
|
||||
-areHooksInstalled : Boolean
|
||||
-isPlanGateEnabled : Boolean
|
||||
}
|
||||
class Template {
|
||||
-name : String
|
||||
-isCopied : Boolean
|
||||
}
|
||||
class InstallResult <<dto>>
|
||||
class Summary {
|
||||
-createdItems : String [0..*]
|
||||
-skippedItems : String [0..*]
|
||||
-nextSteps : String [0..*]
|
||||
}
|
||||
|
||||
ProjectCreator ..> ConfigLoader : creates
|
||||
ProjectCreator ..> ToolChecker : creates
|
||||
ProjectCreator ..> Preflight : creates
|
||||
ProjectCreator ..> GiteaClient : creates
|
||||
ProjectCreator ..> GitHubClient : creates [0..1]
|
||||
ProjectCreator ..> LocalProjectBuilder : creates
|
||||
ProjectCreator ..> FrameworkInstaller : creates
|
||||
ProjectCreator ..> SummaryReport : creates
|
||||
Preflight ..> GiteaClient : asks
|
||||
Preflight ..> GitHubClient : asks [0..1]
|
||||
GitHost <|-- GiteaClient
|
||||
GitHost <|-- GitHubClient
|
||||
GitHost "1" --> "0..*" Owner : has
|
||||
GiteaClient ..> Configuration
|
||||
GitHubClient ..> Configuration
|
||||
|
||||
ProjectCreator --> "1" Run
|
||||
Run *-- "1" Configuration
|
||||
Run *-- "1" ToolCheck
|
||||
Run *-- "0..1" ProjectRequest
|
||||
Run --> "1" PromptSet : returns
|
||||
Configuration *-- "1..2" Credential
|
||||
|
||||
ProjectRequest --> "1" Owner : giteaOwner
|
||||
ProjectRequest --> "0..1" Owner : githubOwner
|
||||
ProjectRequest *-- "0..1" PreflightResult
|
||||
ProjectRequest --> "0..1" GiteaRepository : stored in
|
||||
ProjectRequest --> "0..1" GitHubRepository : also stored in
|
||||
ProjectRequest --> "0..1" LocalProject : working copy
|
||||
Summary --> "1" ProjectRequest : reports on
|
||||
|
||||
Repository <|-- GiteaRepository
|
||||
Repository <|-- GitHubRepository
|
||||
Repository --> "1" Owner : owned by
|
||||
GiteaRepository *-- "0..1" LicenseFile
|
||||
PushMirror --> "1" GiteaRepository : source
|
||||
PushMirror --> "1" GitHubRepository : target
|
||||
PushMirror --> "1" Credential : authorised by
|
||||
|
||||
LocalProject *-- "1..2" Remote
|
||||
Remote --> "1" Repository : points to
|
||||
LocalProject *-- "1" Submodule
|
||||
LocalProject *-- "1" HookSetup
|
||||
LocalProject *-- "0..*" Template
|
||||
InstallResult --> "1" Submodule
|
||||
InstallResult --> "1" HookSetup
|
||||
InstallResult --> "0..*" Template
|
||||
|
||||
ProjectRequest --> Visibility
|
||||
Repository --> Visibility
|
||||
@enduml
|
||||
```
|
||||
|
||||
## Class Table
|
||||
|
||||
| Class | Refines (Domain Model concept) | Responsibility | Attributes | Operations |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `ProjectCreator` | none (controller for the system operations of [OC-001]) | Receives the two system operations, sequences the steps and stops on the first failure. | none | `startProjectCreation`, `provideProjectDetails` |
|
||||
| `ConfigLoader` | Configuration | Reads `config.env` and `.env` as plain text and validates every value, preset project details included. | none | `load` |
|
||||
| `ToolChecker` | none (system concept `ToolCheck`) | Detects the required and optional tools. | none | `check` |
|
||||
| `Preflight` | none (system concept `PreflightResult`) | Runs the read-only checks of both hosts before anything is created. | none | `check` |
|
||||
| `GitHost` | Git Host | The operations every host offers: check the token, check that an owner accepts new repositories, check that a name is free. | `name`, `webAddress`, `apiAddress` | `verifyToken`, `ownerAccepts`, `nameFree` |
|
||||
| `GiteaClient` | Git Host (Gitea) | Hides the Gitea API and its token; creates the repository and the push mirror. | none beyond `GitHost` (uses `Configuration`) | `GiteaClient`, `hasLicense`, `createRepository`, `addPushMirror`, `requestSync` |
|
||||
| `GitHubClient` | Git Host (GitHub) | Hides the GitHub API and its token; creates the empty repository. | none beyond `GitHost` (uses `Configuration`) | `GitHubClient`, `createEmptyRepository` |
|
||||
| `LocalProjectBuilder` | Local Project, Remote | Creates the project directory, its git repository and its credential-free remotes. | none | `build` |
|
||||
| `FrameworkInstaller` | Framework, Framework Setup, Template | Adds the framework submodule, installs skills and hooks once, and copies the templates without overwriting. | none | `install` |
|
||||
| `SummaryReport` | Summary | Composes the report of what was created, skipped or failed. | none | `compose` |
|
||||
| `Run` | none (system concept) | Holds the state of one execution. | `isApply` | none |
|
||||
| `Configuration` | Configuration | Holds the service addresses, the credentials and any preset project details. | `giteaUrl`, `giteaApiUrl`, `githubWebUrl`, `githubApiUrl`, `giteaSshPort`, `mirrorInterval`, `frameworkRepo`, `presetDetails` | none |
|
||||
| `Credential` | Access Token | Holds a secret in memory only; it never becomes part of an address or a message. | `kind`, `value` | none |
|
||||
| `ToolCheck` | none (system concept) | Records which tools are present. | `hasGit`, `hasCurl`, `hasJq` | none |
|
||||
| `PromptSet` | none (system concept) | The questions still to ask; a detail preset in `config.env` is not in it. | `prompts` | none |
|
||||
| `ProjectRequest` | Project | Holds the details of the project being created. | `name`, `description`, `visibility`, `directory`, `enablePlanGate` | none |
|
||||
| `Owner` | Owner | A user or organization on a host. | `name`, `kind` | none |
|
||||
| `PreflightResult` | none (system concept) | Records the outcome of the preflight checks. | `tokensWork`, `ownersAccept`, `nameIsFree`, `licenseIsOffered`, `sshPassed` | none |
|
||||
| `Repository` | Repository | Common data of a repository on a host. | `name`, `description`, `visibility`, `address` | none |
|
||||
| `GiteaRepository` | Gitea Repository | The source of truth. | none beyond `Repository` | none |
|
||||
| `GitHubRepository` | GitHub Repository | Receives its content from the mirror. | none beyond `Repository` | none |
|
||||
| `LicenseFile` | License | The `AGPL-3.0` file in the Gitea repository when GitHub is chosen. | `key` | none |
|
||||
| `PushMirror` | Mirror | The Gitea to GitHub push mirror. | `interval`, `syncOnCommit` | none |
|
||||
| `LocalProject` | Local Project | The project directory on the Maintainer's machine. | `directory` | none |
|
||||
| `Remote` | Remote | A named link to a repository (`origin`, `github`), without a credential. | `name`, `address` | none |
|
||||
| `Submodule` | Framework | The framework added to the local project. | `name`, `address` | none |
|
||||
| `HookSetup` | Framework Setup | Records the skills and hooks installed and the plan gate state. | `areSkillsInstalled`, `areHooksInstalled`, `isPlanGateEnabled` | none |
|
||||
| `Template` | Template | A framework file copied into the project. | `name`, `isCopied` | none |
|
||||
| `InstallResult` | none (carries the result of one operation) | Returns the submodule, the hook setup and the templates of `install`. | none | none |
|
||||
| `Summary` | Summary | The report returned to the Maintainer; it contains no credential. | `createdItems`, `skippedItems`, `nextSteps` | none |
|
||||
| `Visibility` | none (enumeration of a Project and Repository attribute) | The two allowed visibilities. | `private`, `public` | none |
|
||||
|
||||
## Method Traceability
|
||||
|
||||
| Method signature | Operation Contract / SD message |
|
||||
| --- | --- |
|
||||
| `ProjectCreator.startProjectCreation() : PromptSet` | [OC-001] `startProjectCreation`; [SD-001] `startProjectCreation()` |
|
||||
| `ProjectCreator.provideProjectDetails(name, description, visibility, giteaOwner, githubOwner, directory, enablePlanGate) : Summary` | [OC-001] `provideProjectDetails`; [SD-001] `provideProjectDetails(...)` |
|
||||
| `ConfigLoader.load(configFile, envFile) : Configuration` | [SD-001] `load(config.env, .env)`; [OC-001] `startProjectCreation` P2 |
|
||||
| `ToolChecker.check(tools) : ToolCheck` | [SD-001] `check(git, curl, jq)`; [OC-001] `startProjectCreation` P3 |
|
||||
| `Preflight.check(request) : PreflightResult` | [SD-001] `check(request)`; [OC-001] `provideProjectDetails` P2 |
|
||||
| `GiteaClient(configuration)` | [SD-001] `new(configuration)` to `GiteaClient` |
|
||||
| `GitHost.verifyToken() : Boolean` | [SD-001] `verifyToken()` from `Preflight` to either client; P2 |
|
||||
| `GitHost.ownerAccepts(owner) : Boolean` | [SD-001] `ownerAccepts(giteaOwner)` and `ownerAccepts(githubOwner)`; P2 |
|
||||
| `GitHost.nameFree(name) : Boolean` | [SD-001] `nameFree(name)` to either client; P2 |
|
||||
| `GiteaClient.hasLicense(key) : Boolean` | [SD-001] `hasLicense(AGPL-3.0)`; P2 |
|
||||
| `GiteaClient.createRepository(request, license) : GiteaRepository` | [SD-001] `createRepository(request, license)`; P3, P4 |
|
||||
| `GiteaClient.addPushMirror(source, target) : PushMirror` | [SD-001] `addPushMirror(giteaRepository, gitHubRepository)`; P6 |
|
||||
| `GiteaClient.requestSync(mirror) : void` | [SD-001] `requestSync(pushMirror)`; P6 |
|
||||
| `GitHubClient(configuration)` | [SD-001] `new(configuration)` to `GitHubClient` |
|
||||
| `GitHubClient.createEmptyRepository(request) : GitHubRepository` | [SD-001] `createEmptyRepository(request)`; P5 |
|
||||
| `LocalProjectBuilder.build(directory, source, target, sshPassed) : LocalProject` | [SD-001] `build(directory, giteaRepository, gitHubRepository, sshPassed)`; P7, P8, P9 |
|
||||
| `FrameworkInstaller.install(project, enablePlanGate) : InstallResult` | [SD-001] `install(localProject, enablePlanGate)`; P10, P11, P12 |
|
||||
| `SummaryReport.compose(request) : Summary` | [SD-001] `compose(projectRequest)`; P13 |
|
||||
|
||||
## Pattern Annotations
|
||||
|
||||
| Pattern | Classes | Rationale |
|
||||
| --- | --- | --- |
|
||||
| Controller (GRASP) | `ProjectCreator` | One entry for the system operations; coordinates and does no HTTP, git or file work itself |
|
||||
| Facade (GoF) | `GitHost`, `GiteaClient`, `GitHubClient` | Each client hides one host's HTTP API and keeps the token inside; no other class sees a credential. `GitHost` holds the operations both share |
|
||||
| Pure Fabrication (GRASP) | `ConfigLoader`, `ToolChecker`, `Preflight`, `LocalProjectBuilder`, `FrameworkInstaller`, `SummaryReport` | No domain concept owns these responsibilities; small units keep cohesion high |
|
||||
| Creator (GRASP) | `ConfigLoader` creates `Configuration`; `GiteaClient` creates `GiteaRepository` and `PushMirror` | The creating class holds the data needed to build the object |
|
||||
| Protection from variations (GRASP) | `GiteaClient`, `GitHubClient`, `ProjectRequest` | The optional GitHub path is decided by the controller; the clients do not know it |
|
||||
| Data Transfer Object (GoF-style) | `InstallResult` | Carries the three results of `install` in one return value |
|
||||
|
||||
## Dependency Check
|
||||
|
||||
No circular dependency. `ProjectCreator` depends on every helper class and no helper depends on it. `Preflight` depends on the two clients; the clients extend `GitHost` and depend only on `Configuration`. The data classes form a tree: `Run` holds `Configuration`, `ToolCheck` and `ProjectRequest`; `ProjectRequest` reaches the repositories and the `LocalProject`; `Summary` points at `ProjectRequest` and nothing points back at it. `Repository` is shared by `Remote` and `PushMirror` without a cycle.
|
||||
|
||||
SOLID check: no class has more than one reason to change (one host API, one kind of local work, one report); the clients can be replaced behind the same operations; the controller depends on the operations, not on how a host or git is called. `ProjectCreator` has two operations and no data, so it is not a god class.
|
||||
|
||||
## Implementation Mapping
|
||||
|
||||
| Design class | Where it lives in `src/` |
|
||||
| --- | --- |
|
||||
| `ProjectCreator` | `create-project.sh` (`main`), `lib/apply.sh` |
|
||||
| `ConfigLoader` | `lib/config.sh` (`load_configuration`), `lib/validate.sh` |
|
||||
| `ToolChecker` | `lib/tools.sh` |
|
||||
| `Preflight` | `lib/preflight.sh` |
|
||||
| `GitHost`, `GiteaClient`, `GitHubClient` | `lib/api.sh`, `lib/http.sh`, `lib/json.sh`, `lib/repositories.sh`, `lib/mirror.sh`, `lib/hosts.sh` |
|
||||
| `LocalProjectBuilder` | `lib/localproject.sh`, `lib/git.sh` |
|
||||
| `FrameworkInstaller` | `lib/framework.sh` |
|
||||
| `SummaryReport` | `lib/steps.sh`, `lib/plan.sh` |
|
||||
| `PromptSet`, `ProjectRequest` | `lib/project.sh`, `lib/prompts.sh` |
|
||||
| `Run`, `Configuration`, `Credential`, `PreflightResult` | the state arrays declared in `lib/constants.sh` |
|
||||
|
||||
---
|
||||
|
||||
[DCD-001]: ./uc-001/dcd.md
|
||||
[DM-002]: ./domain-model.md
|
||||
[UC-001]: ./uc-001/uc.md
|
||||
[OC-001]: ./uc-001/oc.md
|
||||
[SD-001]: ./uc-001/sd.md
|
||||
[DICT-001]: ./dictionary.md
|
||||
Reference in New Issue
Block a user