輸出模板

各步驟輸出的標準模板。依脈絡調整,但這些是預設。


標準輸出檔案:spec/{feature}/{feature}.ooa.md

全域編碼:對外契約面(Domain Service / Calculator / Strategy 介面 / Domain Exception 等公開型別)是本階段「對外」項目,於詞彙表/類別圖該型別處掛全域碼 §{SHORT}-OA-{N}(N=per-spec 連續整數,無既有本地號故新編)。{SHORT}spec-codes.md。引用上游 FS 邊界條件時用其全域碼 §{SHORT}-FS-{n};引用本地內部節點維持原樣。格式見 workflow §可追溯性「全域編碼」。

把步驟 1~3 的產物組裝成單一檔案,結構如下:

# {功能名稱} OOA
 
> 來源:[fp]({feature}.fp.md)
> 上游消費:{列出消費的上游型別+其全域碼,如 `Resource`/`WorkCapability``§DM-OA-4/8`);無則略}
> 下游:[pseudo]({feature}.pseudo.md)(尚未建立)
> 性質:{單一 feature OOA/跨 feature 共用 OOA(Published Language 的行為面);一句話講消費關係}
> 狀態:待簽核 / 已簽核
 
---
 
## 1. 領域詞彙(Ubiquitous Language)
 
(步驟 1 的詞彙表)
 
## 2. 關係草圖
 
(步驟 1 的純文字關係圖)
 
## 3. 類別圖
 
(步驟 2 的 Mermaid `classDiagram`,含必要的子圖)
 
## 4. SOLID 審查 + 設計模式
 
(步驟 3 的 SOLID 檢查與模式採用結論)
 
## 5. functional spec 對照
 
| Use Case / 邊界條件 | 對應物件職責 |
|---|---|
| UC A1 行為步驟 1~3 | `Scheduler.run()``§{SHORT}-OA-1`) |
| `§{SHORT}-FS-2` 數量 ≤ 0 | `Workorder.validateQuantity()` |
 
## 簽核
 
- **編輯者**____ / 日期:____
- **Reviewer**____ / 日期:____

步驟 4(程式碼骨架)不寫進 spec/{feature}/{feature}.ooa.md,產出獨立 Java 檔。


連結慣例(GFM,寫檔時遵循)

完整規則見 product-module-development-workflow.md §可追溯性「連結慣例(GFM)」。本階段重點:

  • breadcrumb / 跨檔:相對路徑 markdown 連結 [文字](../path.md)(本檔在 spec/{feature}/(兩層深);同 feature 的 fp/pseudo 為同層 sibling、直接用檔名)。
  • 同檔章節跳轉(如 §類別圖、§SOLID 審查):markdown 連結 [§類別圖](#3-類別圖)
  • functional spec 對照表裡回指 spec 的 UC / 邊界條件:可用 markdown 連結到 spec 檔的對應區段,如 [UC A1]({feature}.fp.md#use-cases);§Bn 在表內無法逐列定位,行內維持純文字即可。
  • 標籤一律含非數字字元;純數字 #1 不是合法 tag。

步驟 1:詞彙表模板

領域詞彙拆成兩張表:沿用既有(消費上游、不重定義,掛上游全域碼)與本模組新建模(本階段對外型別掛 §{SHORT}-OA-{N}、純內部 VO 維持本地號)。領域術語對齊 vault 根 glossary.md 的標準形;遇 glossary 沒有的新領域術語,提醒使用者登記。

## 1. 領域詞彙(Ubiquitous Language)
 
### 沿用既有(消費、不重定義)
 
| 詞彙 | 英文 | 出處 | 在本模組的角色 |
|---|---|---|---|
| 工序節點 | `VSMNode` | `§DM-OA-3` | 篩選輸入 key |
| 資源 | `Resource` | `§DM-OA-4` | 候選與輸出主體 |
 
### 本模組新建模
 
| 詞彙 | 英文 | 全域碼 | 說明 | 對應 FS |
|---|---|---|---|---|
| 資源篩選器 | `ResourceSelector` | `§{SHORT}-OA-1` | Domain Service 門面 | 整份 |
| 篩選 key | `ResourceSelectionKey`(VO) | —(內部) | 純內部導航 VO,維持本地號、不掛全域碼 | §{SHORT}-FS-6 |

純內部導航的 VO 全域碼欄填 —(內部),維持本地號(兩層並存,見 spec-codes.md)。

接著用純文字呈現關係草圖

WorkCenter
   ▲
   │ belongs to
   │
Resource ──has 1──> Schedule
   │
   ├── has N ──> Capability
   │
   └── located at ──> Location

最後,攤開 2-3 個設計分歧,讓使用者表態。


步驟 2:Mermaid 類別圖模板

classDiagram
    direction TB

    %% ========== Core Abstraction ==========
    class Resource {
        <<abstract>>
        -resourceId: ResourceId
        -name: String
        -schedule: Schedule
        +isOnDuty(dateTime) boolean
        +hasCapability(process) boolean
    }

    class Machine {
        -machineType: String
        -automationLevel: AutomationLevel
    }

    Resource <|-- Machine

    %% ========== Enumerations ==========
    class AutomationLevel {
        <<enumeration>>
        MANUAL
        SEMI_AUTO
        FULL_AUTO
    }
    Machine --> AutomationLevel

    %% ========== Schedule ==========
    class Schedule {
        -baselineShifts: List~RecurringShift~
        -exceptions: List~CalendarException~
        +getWorkingSlots(date) List~TimeSlot~
        +addException(exception) void
    }
    Resource "1" *-- "1" Schedule

    %% ========== Specifications ==========
    class ResourceRequirement {
        <<interface>>
        +isSatisfiedBy(resource) boolean
        +and(other) ResourceRequirement
    }

關鍵慣例

  • 一律 direction TB,除非有理由用 LR
  • %% ========== 註解把相關類別分組
  • 在關係上標出多重性("1""*""1..*"
  • 泛型用 ~Type~(因為 <> 在某些情境會讓 Mermaid 混淆)
  • 靜態方法在某些 Mermaid 渲染器中加 $ 後綴:+of(start, end)$ TimeSlot

步驟 3:SOLID 審查模板

## SOLID Audit
 
### ✅ 設計穩固之處
 
- **SRP**`Schedule` 只處理時間;`Capability` 只描述能力。
- **OCP**`CalendarException` 子類別允許新增例外型別而不修改 `Schedule`
- **DIP**`MatchingRule` 依賴 `ResourceRepository` 介面,而非具體資料儲存。
 
### ⚠️ 潛在風險
 
1. **`Resource.hasCapability(Process)` 的 ISP 風險**
   - 對 Worker 而言,「capability」語意上很怪——工人是以技能在思考。
   - **修法選項 A**:改名成更抽象的詞
   - **修法選項 B**:只為 Machine/Mold 萃取 `ProcessExecutor` 介面
 
2. **`Resource.isAvailable()` 的 SRP 風險**
   - 可能被解讀為「是否在班」或「目前是否未被排程」——這是不同的關注點。
   - **建議**:改名為 `isOnDuty()`,並把佔用追蹤留在排程模組。
 
## 建議的設計模式
 
| # | 模式 | 用於 | 必要性 |
|---|---------|----------|-----------|
| 1 | Specification | 可組合的 `ResourceRequirement` | 🔥 強烈推薦 |
| 2 | Repository | `ResourceRepository` 介面 | ✅ DDD 標準 |
| 3 | Template Method | `CalendarException.applyTo()` | ✅ 自然契合 |
| 4 | Composite | 操作關係(machine + mold + worker) | ⭐ 僅在需要時 |
 
### 要採用哪些?
 
[在此使用 ask_user_input_v0]

步驟 4:Java 程式碼骨架模板

純 POJO 風格(無框架、無 Lombok、無 record)

package mydomain.calendar;
 
import java.time.*;
import java.util.List;
 
// 班次:星期幾的某段工作時間(含休息)
public class RecurringShift {
    // 欄位:
    //   String shiftId
    //   DayOfWeek dayOfWeek
    //   LocalTime startTime
    //   Duration duration
    //   List<BreakPeriod> breaks
    //   ShiftType shiftType
 
    // 是否跨日(startTime + duration 超過 24:00)
    public boolean isOvernight() {
        return startTime.toSecondOfDay() + duration.getSeconds() > 86400L;
    }
 
    // 在指定日期上展開:去除休息時段後的 TimeSlot 列表
    public List<TimeSlot> materialize(LocalDate date) {
        // 1. 起始時段
        // 2. 扣除每個休息時段
        // 3. 回傳
    }
}

Specification 模式(帶 default method 的介面)

public interface ResourceRequirement {
 
    boolean isSatisfiedBy(Resource resource);
 
    default ResourceRequirement and(ResourceRequirement other) {
        return new AndRequirement(this, other);
    }
 
    default ResourceRequirement or(ResourceRequirement other) {
        return new OrRequirement(this, other);
    }
 
    default ResourceRequirement not() {
        return new NotRequirement(this);
    }
}
 
public class AndRequirement implements ResourceRequirement {
    private final ResourceRequirement left;
    private final ResourceRequirement right;
 
    public AndRequirement(ResourceRequirement left, ResourceRequirement right) {
        this.left = Objects.requireNonNull(left);
        this.right = Objects.requireNonNull(right);
    }
 
    @Override
    public boolean isSatisfiedBy(Resource resource) {
        return left.isSatisfiedBy(resource) && right.isSatisfiedBy(resource);
    }
}

帶前置條件檢查的 Aggregate Root

public class Schedule {
    // 欄位:
    //   List<RecurringShift> baselineShifts
    //   List<CalendarException> exceptions
 
    public void addException(CalendarException exception) {
        for (CalendarException existing : exceptions) {
            if (existing.conflictsWith(exception)) {
                throw new CalendarConflictException(
                    "新增的例外 " + exception.getExceptionId()
                    + " 與既有例外 " + existing.getExceptionId() + " 衝突");
            }
        }
        exceptions.add(exception);
    }
}

帶 factory method 的不可變 Value Object

public final class TimeSlot {
    private final LocalDateTime start;
    private final LocalDateTime end;
 
    public static TimeSlot of(LocalDateTime start, LocalDateTime end) {
        if (!start.isBefore(end)) {
            throw new IllegalArgumentException("時段起點必須早於終點");
        }
        return new TimeSlot(start, end);
    }
 
    private TimeSlot(LocalDateTime start, LocalDateTime end) {
        this.start = start;
        this.end = end;
    }
 
    public LocalDateTime start() { return start; }
    public LocalDateTime end() { return end; }
    public Duration duration() { return Duration.between(start, end); }
 
    @Override public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof TimeSlot other)) return false;
        return start.equals(other.start) && end.equals(other.end);
    }
 
    @Override public int hashCode() { return Objects.hash(start, end); }
}

JUnit 5 測試模板

package mydomain.calendar;
 
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
 
class RecurringShiftMaterializeTest {
 
    @Test
    void overnightShiftWithBreakShouldSplitCorrectly() {
        // 情境:週五 20:00 起 7 小時(跨到週六 03:00),中間 23:00 休 30 分
        // 期望:兩段 TimeSlot
 
        BreakPeriod brk = new BreakPeriod(
            LocalTime.of(23, 0), Duration.ofMinutes(30), BreakType.REST);
 
        RecurringShift shift = new RecurringShift(
            "FRI-NIGHT", DayOfWeek.FRIDAY,
            LocalTime.of(20, 0), Duration.ofHours(7),
            List.of(brk), ShiftType.NIGHT);
 
        assertTrue(shift.isOvernight());
 
        List<TimeSlot> slots = shift.materialize(LocalDate.of(2026, 5, 22));
        assertEquals(2, slots.size());
        assertTrue(slots.get(1).spansMidnight());
    }
}

Markdown 文件模板(步驟 4 的替代選項)

若使用者想要 markdown 文件,而非(或同時要)程式碼,組織如下:

project-docs/
├── CLAUDE.md                    主入口 + 開發指引
├── README.md                    模組總覽
├── docs/
│   ├── 01-domain-vocabulary.md
│   ├── 02-class-diagram.md
│   ├── 03-design-decisions.md
│   └── 04-package-structure.md
└── specs/
    └── <subdomain>.md           每個子領域一份實作 spec

CLAUDE.md 模板

# CLAUDE.md — [Module Name]
 
> 給 Claude Code 的開發指引。請先讀完本檔,再依任務需求閱讀對應的 spec。
 
## 專案定位
[這個模組是什麼、做什麼、不做什麼]
 
## 技術棧
- Java [version]
- [framework or "no framework"]
- [other dependencies]
 
## 開發原則
1. [風格規則,例如 POJO、不要 getter/setter]
2. [命名慣例,例如程式碼英文、註解中文]
3. [依賴隔離規則]
4. [測試慣例]
 
## 開發順序建議
[建議的實作順序,逐 package 列]
 
## 文件導覽
[「我想做 X」→「讀 Y」的對照表]

子 package spec 模板

# Spec: [package.name]
 
> [一行描述]
 
## 套件: `package.name`
 
依賴:
- [列出依賴]
 
---
 
## ClassName
 
**性質**:[Entity / Value Object / Aggregate Root / Service]
 
**欄位**
- `Type fieldName` — 說明
 
**方法**
- `method(param) ReturnType` — 行為說明
 
**規則**
- [不變式與約束]
 
**範例**(如有需要)
[程式碼範例]
 
---
 
## 測試重點
 
### `TestClassName`
 
[帶註解的測試方法骨架]

何時產生檔案 vs. 行內輸出

情況輸出方式
使用者要求行內審查/分析在對話中行內輸出 markdown
使用者說「整理成 markdown」/「我要檔案」/「幫我做文件」用 create_file 產生 .md
使用者要可編譯的程式碼產生 .java
只是在討論設計行內輸出(別過早產生檔案)

產生檔案時,使用上方所示的目錄結構,並在最後呼叫 present_files 把它們分享給使用者。