aizuchi 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +149 -0
- package/dist/bin/aizuchi.js +546 -0
- package/dist/server/app.js +817 -0
- package/dist/server/board.js +248 -0
- package/dist/server/events.js +24 -0
- package/dist/server/lib/config-paths.js +91 -0
- package/dist/server/lib/docs.js +154 -0
- package/dist/server/lib/files.js +208 -0
- package/dist/server/lib/fs-safe.js +428 -0
- package/dist/server/lib/git.js +100 -0
- package/dist/server/lib/id.js +54 -0
- package/dist/server/lib/pinpoint.js +225 -0
- package/dist/server/lib/proposal.js +93 -0
- package/dist/server/lib/task-file.js +132 -0
- package/dist/server/lib/task-ops.js +206 -0
- package/dist/server/lib/time.js +24 -0
- package/dist/server/overview.js +119 -0
- package/dist/server/static.js +71 -0
- package/dist/server/watcher.js +38 -0
- package/dist/shared/critical-path.js +128 -0
- package/dist/shared/diff.js +46 -0
- package/dist/shared/digest.js +44 -0
- package/dist/shared/milestone.js +29 -0
- package/dist/shared/related-docs.js +77 -0
- package/dist/shared/schema.js +376 -0
- package/dist/shared/stats.js +167 -0
- package/dist/web/assets/index-BLN_aXBe.css +1 -0
- package/dist/web/assets/index-BS_n8d9B.js +82 -0
- package/dist/web/index.html +13 -0
- package/package.json +73 -0
- package/templates/claude-rules.md +85 -0
- package/templates/config.json +28 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 日次ダイジェスト (波 II)。全タスクの作業ログを日付でグルーピングして時系列にマージする純関数。
|
|
3
|
+
* 「その日に何が起きたか」を 1 画面で見るための集計。追加依存なし。
|
|
4
|
+
*
|
|
5
|
+
* 作業ログ行の書式: `- YYYY-MM-DD HH:mm [member] msg` (§5.3)。
|
|
6
|
+
*/
|
|
7
|
+
// `- 2026-06-10 10:05 [claude] msg` を分解する (作業ログ以外のセクションの時刻付き行も拾う)
|
|
8
|
+
const LOG_LINE_RE = /^\s*[-*]\s*(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2})\s+\[([^\]]+)\]\s*(.*)$/;
|
|
9
|
+
/**
|
|
10
|
+
* タスク本文から作業ログ行を本文中の出現順で抽出する (digest と stats の唯一の parse 源)。
|
|
11
|
+
* 書式は §5.3 の `- YYYY-MM-DD HH:mm [member] msg`。LOG_LINE_RE を再実装しないための共有ヘルパ。
|
|
12
|
+
*/
|
|
13
|
+
export function parseWorkLogLines(body) {
|
|
14
|
+
const lines = [];
|
|
15
|
+
for (const line of body.split('\n')) {
|
|
16
|
+
const m = line.match(LOG_LINE_RE);
|
|
17
|
+
if (!m)
|
|
18
|
+
continue;
|
|
19
|
+
const [, date, time, member, message] = m;
|
|
20
|
+
lines.push({ date, time, member, message });
|
|
21
|
+
}
|
|
22
|
+
return lines;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* 全タスクの本文から作業ログ行を抽出し、日付ごとにまとめて新しい日付順 (降順) で返す。
|
|
26
|
+
* 同一日内は時刻昇順 → taskId 昇順で安定ソート。
|
|
27
|
+
*/
|
|
28
|
+
export function buildDigest(tasks) {
|
|
29
|
+
const byDate = new Map();
|
|
30
|
+
for (const task of tasks) {
|
|
31
|
+
for (const { date, time, member, message } of parseWorkLogLines(task.body)) {
|
|
32
|
+
const list = byDate.get(date) ?? [];
|
|
33
|
+
list.push({ taskId: task.id, time, member, message });
|
|
34
|
+
byDate.set(date, list);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const days = [...byDate.entries()].map(([date, entries]) => ({
|
|
38
|
+
date,
|
|
39
|
+
entries: entries.sort((a, b) => a.time.localeCompare(b.time) || a.taskId.localeCompare(b.taskId, 'en', { numeric: true })),
|
|
40
|
+
}));
|
|
41
|
+
// 新しい日付が上 (降順)
|
|
42
|
+
days.sort((a, b) => b.date.localeCompare(a.date));
|
|
43
|
+
return days;
|
|
44
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* マイルストーン定義とタスクから進捗を集計する。
|
|
3
|
+
* 存在しない milestone ID を指すタスクは unassigned に倒す (定義漏れの可視化は呼び出し側判断)。
|
|
4
|
+
*/
|
|
5
|
+
export function aggregateMilestones(defs, tasks) {
|
|
6
|
+
// 重複 ID の milestone 定義は最初の 1 件のみ採用する (R7: 二重計上を防ぐ)
|
|
7
|
+
const uniqueDefs = [];
|
|
8
|
+
const seen = new Set();
|
|
9
|
+
for (const m of defs) {
|
|
10
|
+
if (seen.has(m.id))
|
|
11
|
+
continue;
|
|
12
|
+
seen.add(m.id);
|
|
13
|
+
uniqueDefs.push(m);
|
|
14
|
+
}
|
|
15
|
+
const counts = new Map();
|
|
16
|
+
for (const m of uniqueDefs)
|
|
17
|
+
counts.set(m.id, { done: 0, total: 0 });
|
|
18
|
+
const unassigned = { done: 0, total: 0 };
|
|
19
|
+
for (const task of tasks) {
|
|
20
|
+
const bucket = task.milestone !== null && seen.has(task.milestone) ? counts.get(task.milestone) : unassigned;
|
|
21
|
+
bucket.total += 1;
|
|
22
|
+
if (task.done)
|
|
23
|
+
bucket.done += 1;
|
|
24
|
+
}
|
|
25
|
+
const milestones = uniqueDefs
|
|
26
|
+
.map((m) => ({ milestone: m, ...counts.get(m.id) }))
|
|
27
|
+
.sort((a, b) => a.milestone.order - b.milestone.order || a.milestone.id.localeCompare(b.milestone.id));
|
|
28
|
+
return { milestones, unassigned };
|
|
29
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/** 指定資料パスを related_docs に含むタスクを ID 昇順で返す (完全一致・正規化は呼び出し側責務) */
|
|
2
|
+
export function tasksForDoc(tasks, docPath) {
|
|
3
|
+
return tasks
|
|
4
|
+
.filter((task) => (task.related_docs ?? []).includes(docPath))
|
|
5
|
+
.map(({ id, title, status }) => ({ id, title, status }))
|
|
6
|
+
.sort((a, b) => a.id.localeCompare(b.id, 'en', { numeric: true }));
|
|
7
|
+
}
|
|
8
|
+
// frontmatter 境界: 先頭の `---` 行 (行末空白を許容し parseTaskFile の判定と揃える、R13-1)
|
|
9
|
+
const FM_OPEN = /^---[ \t]*\r?\n/;
|
|
10
|
+
// 閉じ `---` 行 (行末空白許容)
|
|
11
|
+
const FM_CLOSE = /^---[ \t]*$/;
|
|
12
|
+
// BOM は literal 禁止 (LEARN#13: invisible unicode は \uXXXX escape で書く)
|
|
13
|
+
const BOM = '\uFEFF';
|
|
14
|
+
/**
|
|
15
|
+
* raw (frontmatter + 本文) の related_docs 行を docs で差し替えた raw を返す。
|
|
16
|
+
* - 先頭 BOM は剥がして処理し、戻り値で復元する (parseTaskFile / applyPinpointPatch と同じ規約、R13-1)
|
|
17
|
+
* - docs が空: related_docs 行 (flow 形式 1 行 or block 形式の key 行 + 続く `- ` 行) を除去
|
|
18
|
+
* - docs が非空: JSON 文字列化した flow 形式 1 行 (`related_docs: ["a", "b"]`) に置換。
|
|
19
|
+
* 既存行が無ければ `created:` 行の直前 (無ければ閉じ `---` の直前) に挿入する。
|
|
20
|
+
* - frontmatter の他の行と本文はバイト保全する。JSON 文字列は YAML の double-quoted スカラとして
|
|
21
|
+
* 常に valid (特殊文字を含むパスも安全)。
|
|
22
|
+
* frontmatter が見つからない raw はそのまま返す (呼び出し側は「変化なし」を検出してエラー表示する)。
|
|
23
|
+
*/
|
|
24
|
+
export function setRelatedDocsInRaw(raw, docs) {
|
|
25
|
+
// BOM を剥がして処理し、最後に復元する (R13-1: BOM 付きタスクで silent no-op にしない)
|
|
26
|
+
const hasBom = raw.startsWith(BOM);
|
|
27
|
+
const body = hasBom ? raw.slice(1) : raw;
|
|
28
|
+
const result = setRelatedDocsInner(body, docs);
|
|
29
|
+
return hasBom ? BOM + result : result;
|
|
30
|
+
}
|
|
31
|
+
function setRelatedDocsInner(raw, docs) {
|
|
32
|
+
if (!FM_OPEN.test(raw))
|
|
33
|
+
return raw;
|
|
34
|
+
const lines = raw.split('\n');
|
|
35
|
+
// 閉じ `---` を探す (行 0 は開き `---`)
|
|
36
|
+
let closeIdx = -1;
|
|
37
|
+
for (let i = 1; i < lines.length; i++) {
|
|
38
|
+
if (FM_CLOSE.test(lines[i].replace(/\r$/, ''))) {
|
|
39
|
+
closeIdx = i;
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (closeIdx === -1)
|
|
44
|
+
return raw;
|
|
45
|
+
// 既存 related_docs 行 (flow 1 行 or block 形式) を frontmatter 内から除去
|
|
46
|
+
const kept = [];
|
|
47
|
+
let i = 1;
|
|
48
|
+
while (i < closeIdx) {
|
|
49
|
+
const line = lines[i].replace(/\r$/, '');
|
|
50
|
+
if (/^related_docs\s*:/.test(line)) {
|
|
51
|
+
i++;
|
|
52
|
+
// block 形式: 続く `- ` 行も取り除く。ゼロインデント (PyYAML 既定・valid YAML) も対象 (R13-4)。
|
|
53
|
+
// 次のトップレベル key 行は `- ` で始まらないため、この条件で安全に打ち切れる
|
|
54
|
+
while (i < closeIdx && /^\s*-\s/.test(lines[i].replace(/\r$/, '')))
|
|
55
|
+
i++;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
kept.push(lines[i]);
|
|
59
|
+
i++;
|
|
60
|
+
}
|
|
61
|
+
// 挿入する新しい行 (docs 非空のときのみ)。JSON.stringify は YAML double-quoted として valid
|
|
62
|
+
const newLine = docs.length > 0 ? `related_docs: [${docs.map((d) => JSON.stringify(d)).join(', ')}]` : null;
|
|
63
|
+
if (newLine !== null) {
|
|
64
|
+
const createdIdx = kept.findIndex((l) => /^created\s*:/.test(l.replace(/\r$/, '')));
|
|
65
|
+
if (createdIdx !== -1) {
|
|
66
|
+
kept.splice(createdIdx, 0, newLine);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
kept.push(newLine);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const head = [lines[0], ...kept].join('\n');
|
|
73
|
+
const tail = lines.slice(closeIdx).join('\n');
|
|
74
|
+
// 改行コードは元 raw のものが保たれる (split('\n') で \r が各行末に残るため CRLF はそのまま。
|
|
75
|
+
// 新規挿入行のみ LF になるが、混在しても YAML としては valid)
|
|
76
|
+
return `${head}\n${tail}`;
|
|
77
|
+
}
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* タスク・config・警告の zod スキーマ。
|
|
4
|
+
* server / web が共有する唯一の型の源 (型は z.infer で導出する)。
|
|
5
|
+
* 要件定義書 §5 のデータ仕様を Phase 1 から全フィールド・全ステータス実装する。
|
|
6
|
+
*/
|
|
7
|
+
export const TASK_STATUSES = ['proposed', 'backlog', 'todo', 'in_progress', 'pending', 'done'];
|
|
8
|
+
export const BLOCKED_TYPES = ['decision', 'external', 'dependency'];
|
|
9
|
+
export const PRIORITIES = ['high', 'medium', 'low'];
|
|
10
|
+
export const MEMBER_TYPES = ['human', 'ai'];
|
|
11
|
+
// マイルストーンの状態 (Phase 2 波 II の新規スキーマ拡張)
|
|
12
|
+
export const MILESTONE_STATUSES = ['planned', 'active', 'done'];
|
|
13
|
+
export const DEFAULT_PORT = 5151;
|
|
14
|
+
/** overview モードの既定ポート (波 VII)。serve の 5151 と衝突しない値・1 箇所集約 */
|
|
15
|
+
export const DEFAULT_OVERVIEW_PORT = 5152;
|
|
16
|
+
// 日付は文字列のまま扱い、Date 化しない (js-yaml JSON_SCHEMA エンジンと対になる規則)。
|
|
17
|
+
// zod の datetime({ offset: true }) を使い、月 13 / 時 25 のような実在しない日時を弾く
|
|
18
|
+
// (自前正規表現は桁数だけ見て実在性を見ず、経過時間表示が NaN になりうる — PM Round 1 Minor #8)。
|
|
19
|
+
export const isoDateTimeSchema = z
|
|
20
|
+
.string()
|
|
21
|
+
.datetime({ offset: true, message: 'ISO 8601 (タイムゾーン付き) の文字列で指定する (例: 2026-06-10T10:00:00+09:00)' });
|
|
22
|
+
// ID = ファイル名 (拡張子除く)。パス区切り・制御文字が入らない形式に限定する
|
|
23
|
+
export const taskIdSchema = z
|
|
24
|
+
.string()
|
|
25
|
+
.regex(/^[A-Za-z0-9_]+-\d{3,}$/, 'タスク ID は <接頭辞>-<3 桁以上の数字> 形式 (例: TASK-001)');
|
|
26
|
+
// 1 行テキスト (title 等。改行を含むと行ベースの frontmatter 処理が壊れる)
|
|
27
|
+
const singleLineString = z.string().min(1).regex(/^[^\r\n]+$/, '1 行で書く (改行不可)');
|
|
28
|
+
export const memberSchema = z.object({
|
|
29
|
+
id: z.string().min(1),
|
|
30
|
+
type: z.enum(MEMBER_TYPES),
|
|
31
|
+
});
|
|
32
|
+
export const columnSchema = z.object({
|
|
33
|
+
id: z.string().min(1),
|
|
34
|
+
label: z.string().min(1),
|
|
35
|
+
hideWhenEmpty: z.boolean().optional(),
|
|
36
|
+
wipLimit: z.number().int().positive().optional(),
|
|
37
|
+
});
|
|
38
|
+
/** マイルストーン定義 (Phase 2 波 II の新規スキーマ拡張)。タスクは frontmatter の milestone で参照する */
|
|
39
|
+
export const milestoneSchema = z.object({
|
|
40
|
+
id: z.string().min(1),
|
|
41
|
+
title: z.string().min(1),
|
|
42
|
+
/** 予定表での表示順 (昇順) */
|
|
43
|
+
order: z.number().int().default(0),
|
|
44
|
+
status: z.enum(MILESTONE_STATUSES).default('planned'),
|
|
45
|
+
});
|
|
46
|
+
/**
|
|
47
|
+
* タスクテンプレート定義 (波 VI)。定型タスクの起票を速くするための雛形。
|
|
48
|
+
* name は選択 UI のラベル (必須)、title/body/labels/priority は起票フォームへのプリフィル値 (任意)。
|
|
49
|
+
* .strict で未知フィールドを拒否する (config の typo を可視化)。既定は空配列で後方互換。
|
|
50
|
+
*/
|
|
51
|
+
export const taskTemplateSchema = z
|
|
52
|
+
.object({
|
|
53
|
+
name: z.string().min(1),
|
|
54
|
+
title: z.string().optional(),
|
|
55
|
+
body: z.string().optional(),
|
|
56
|
+
labels: z.array(z.string().min(1)).optional(),
|
|
57
|
+
priority: z.enum(PRIORITIES).optional(),
|
|
58
|
+
})
|
|
59
|
+
.strict();
|
|
60
|
+
export const configSchema = z.object({
|
|
61
|
+
projectName: z.string().min(1).default('project'),
|
|
62
|
+
idPrefix: z
|
|
63
|
+
.string()
|
|
64
|
+
.regex(/^[A-Za-z0-9_]+$/, 'idPrefix は英数字とアンダースコアのみ')
|
|
65
|
+
.default('TASK'),
|
|
66
|
+
port: z.number().int().min(1).max(65535).default(DEFAULT_PORT),
|
|
67
|
+
aiTaskCreation: z.enum(['free', 'proposal']).default('free'),
|
|
68
|
+
members: z
|
|
69
|
+
.array(memberSchema)
|
|
70
|
+
.min(1)
|
|
71
|
+
.default([
|
|
72
|
+
{ id: 'kurata', type: 'human' },
|
|
73
|
+
{ id: 'fujimaki', type: 'human' },
|
|
74
|
+
{ id: 'claude', type: 'ai' },
|
|
75
|
+
{ id: 'codex', type: 'ai' },
|
|
76
|
+
]),
|
|
77
|
+
columns: z
|
|
78
|
+
.array(columnSchema)
|
|
79
|
+
.min(1)
|
|
80
|
+
.default([
|
|
81
|
+
{ id: 'proposed', label: '提案', hideWhenEmpty: true },
|
|
82
|
+
{ id: 'backlog', label: 'Backlog' },
|
|
83
|
+
{ id: 'todo', label: 'Todo' },
|
|
84
|
+
{ id: 'in_progress', label: 'In Progress', wipLimit: 2 },
|
|
85
|
+
{ id: 'pending', label: 'Pending' },
|
|
86
|
+
{ id: 'done', label: 'Done' },
|
|
87
|
+
]),
|
|
88
|
+
instructionTemplate: z
|
|
89
|
+
.string()
|
|
90
|
+
.default('タスク {id} の内容を {path} で確認し、CLAUDE.md の運用規約に従って実行してください。'),
|
|
91
|
+
aiConfigPaths: z
|
|
92
|
+
.object({
|
|
93
|
+
settingsFiles: z.array(z.string()).default(['.claude/settings.json', '.claude/settings.local.json']),
|
|
94
|
+
browseDirs: z.array(z.string()).default(['.claude/skills', '.claude/rules', '.claude/agents']),
|
|
95
|
+
rootFiles: z.array(z.string()).default(['CLAUDE.md']),
|
|
96
|
+
})
|
|
97
|
+
.default({}),
|
|
98
|
+
// マイルストーン定義 (Phase 2 波 II 拡張)。既定は空 = 既存 config と後方互換
|
|
99
|
+
milestones: z.array(milestoneSchema).default([]),
|
|
100
|
+
// タスクテンプレート定義 (波 VI)。既定は空 = 既存 config と後方互換 (テンプレート UI は非表示)
|
|
101
|
+
taskTemplates: z.array(taskTemplateSchema).default([]),
|
|
102
|
+
// 関連資料ディレクトリ (波 VIII)。プロジェクトルートからの相対パス。既定は空 = 機能オフ (UI 非表示)
|
|
103
|
+
docsDirs: z.array(z.string().min(1)).default([]),
|
|
104
|
+
});
|
|
105
|
+
/** 既定 config を生成する (N-6: 設定ゼロでも既定値で動作する) */
|
|
106
|
+
export function defaultConfig() {
|
|
107
|
+
return configSchema.parse({});
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* タスク frontmatter (要件 §5.3)。
|
|
111
|
+
* 未知フィールドは .strict() でエラーにする (§8-7「独自フィールドを追加しない」の書式ミス検出)。
|
|
112
|
+
* pending 時の blocked_type / blocked_reason 必須は superRefine で表現する。
|
|
113
|
+
*/
|
|
114
|
+
export const taskFrontmatterSchema = z
|
|
115
|
+
.object({
|
|
116
|
+
id: taskIdSchema,
|
|
117
|
+
title: singleLineString, // §5.3「1 行タイトル」
|
|
118
|
+
status: z.enum(TASK_STATUSES),
|
|
119
|
+
assignee: z.string().min(1).nullable().default(null),
|
|
120
|
+
created_by: z.string().min(1),
|
|
121
|
+
priority: z.enum(PRIORITIES).default('medium'),
|
|
122
|
+
labels: z.array(z.string().min(1)).default([]),
|
|
123
|
+
blocked_type: z.enum(BLOCKED_TYPES).nullable().default(null),
|
|
124
|
+
blocked_reason: z.string().min(1).nullable().default(null),
|
|
125
|
+
depends_on: z.array(taskIdSchema).default([]),
|
|
126
|
+
// Phase 2 波 II 拡張: 所属マイルストーン (config.milestones の id)。任意・後方互換 (既存タスクは null)
|
|
127
|
+
milestone: z.string().min(1).nullable().default(null),
|
|
128
|
+
// 波 VIII 拡張: 関連資料 (プロジェクトルートからの相対パス配列)。任意・後方互換 (milestone 前例)。
|
|
129
|
+
// serialize 時は未指定 / 空なら行ごと省略し、既存ファイルと差分ゼロ
|
|
130
|
+
related_docs: z.array(z.string().min(1)).optional(),
|
|
131
|
+
created: isoDateTimeSchema,
|
|
132
|
+
updated: isoDateTimeSchema,
|
|
133
|
+
})
|
|
134
|
+
.strict()
|
|
135
|
+
.superRefine((task, ctx) => {
|
|
136
|
+
if (task.status !== 'pending')
|
|
137
|
+
return;
|
|
138
|
+
if (task.blocked_type === null) {
|
|
139
|
+
ctx.addIssue({
|
|
140
|
+
code: z.ZodIssueCode.custom,
|
|
141
|
+
path: ['blocked_type'],
|
|
142
|
+
message: 'pending のタスクは blocked_type (decision / external / dependency) が必須',
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
if (task.blocked_reason === null) {
|
|
146
|
+
ctx.addIssue({
|
|
147
|
+
code: z.ZodIssueCode.custom,
|
|
148
|
+
path: ['blocked_reason'],
|
|
149
|
+
message: 'pending のタスクは blocked_reason (誰の・何待ちか) が必須',
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
/**
|
|
154
|
+
* created_by / assignee が config.members に登録済みかを検証する (F-6-2)。
|
|
155
|
+
* スキーマ単体では members を知れないため、config を引数に取る関数として提供する。
|
|
156
|
+
* 戻り値は違反メッセージの配列 (空 = 違反なし)。
|
|
157
|
+
*/
|
|
158
|
+
export function findMemberViolations(task, config) {
|
|
159
|
+
const registered = new Set(config.members.map((member) => member.id));
|
|
160
|
+
const violations = [];
|
|
161
|
+
if (!registered.has(task.created_by)) {
|
|
162
|
+
violations.push(`created_by "${task.created_by}" は config.members に未登録`);
|
|
163
|
+
}
|
|
164
|
+
if (task.assignee !== null && !registered.has(task.assignee)) {
|
|
165
|
+
violations.push(`assignee "${task.assignee}" は config.members に未登録`);
|
|
166
|
+
}
|
|
167
|
+
return violations;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* ボード上の警告バナーに出す警告 (F-6-2: 不正ファイル・重複 ID・members 未登録・config 不正・
|
|
171
|
+
* 提案モード違反 F-3-4)。
|
|
172
|
+
*/
|
|
173
|
+
export const WARNING_KINDS = [
|
|
174
|
+
'invalid_task',
|
|
175
|
+
'duplicate_id',
|
|
176
|
+
'unregistered_member',
|
|
177
|
+
'invalid_config',
|
|
178
|
+
'mode_violation',
|
|
179
|
+
];
|
|
180
|
+
export const warningSchema = z.object({
|
|
181
|
+
kind: z.enum(WARNING_KINDS),
|
|
182
|
+
/** プロジェクトルートからの相対パス */
|
|
183
|
+
path: z.string(),
|
|
184
|
+
/** 対象タスク ID (特定できない場合は null) */
|
|
185
|
+
taskId: z.string().nullable(),
|
|
186
|
+
message: z.string(),
|
|
187
|
+
});
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
// API 境界型 (議事録「境界型 4 つ」のデータフロー図に対応)
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
/** GET /api/board が返すタスク 1 件分のビュー */
|
|
192
|
+
export const taskViewSchema = z.object({
|
|
193
|
+
frontmatter: taskFrontmatterSchema,
|
|
194
|
+
/** プロジェクトルートからの相対パス */
|
|
195
|
+
path: z.string(),
|
|
196
|
+
/** frontmatter 以降の本文 (プレビュー用) */
|
|
197
|
+
body: z.string(),
|
|
198
|
+
/** ファイル全文 (詳細パネルの編集用) */
|
|
199
|
+
raw: z.string(),
|
|
200
|
+
/** PUT の baseMtimeMs に使う厳密値 */
|
|
201
|
+
mtimeMs: z.number(),
|
|
202
|
+
});
|
|
203
|
+
/** GET /api/board のレスポンス */
|
|
204
|
+
export const boardResponseSchema = z.object({
|
|
205
|
+
tasks: z.array(taskViewSchema),
|
|
206
|
+
config: configSchema,
|
|
207
|
+
warnings: z.array(warningSchema),
|
|
208
|
+
});
|
|
209
|
+
/** POST /api/tasks の入力 (status はサーバーが todo を採用、ID はサーバーが採番) */
|
|
210
|
+
export const newTaskInputSchema = z
|
|
211
|
+
.object({
|
|
212
|
+
title: singleLineString,
|
|
213
|
+
created_by: z.string().min(1),
|
|
214
|
+
assignee: z.string().min(1).nullable().optional(),
|
|
215
|
+
priority: z.enum(PRIORITIES).optional(),
|
|
216
|
+
labels: z.array(z.string().min(1)).optional(),
|
|
217
|
+
// 本文 (波 VI: テンプレートからのプリフィル用)。省略時はサーバーが既定本文テンプレートを使う
|
|
218
|
+
body: z.string().optional(),
|
|
219
|
+
})
|
|
220
|
+
.strict();
|
|
221
|
+
/**
|
|
222
|
+
* PATCH /api/tasks/:id の入力 (D&D。pending 行きのみ blocked_* を同梱する)。
|
|
223
|
+
* blocked_reason はダイアログ入力 (1 行) を想定して改行を拒否する
|
|
224
|
+
* (AI のファイル直編集による複数行 blocked_reason は parse / pinpoint 側が対応済み)。
|
|
225
|
+
*/
|
|
226
|
+
export const moveInputSchema = z
|
|
227
|
+
.object({
|
|
228
|
+
status: z.enum(TASK_STATUSES),
|
|
229
|
+
blocked_type: z.enum(BLOCKED_TYPES).nullable().optional(),
|
|
230
|
+
blocked_reason: singleLineString.nullable().optional(),
|
|
231
|
+
})
|
|
232
|
+
.strict()
|
|
233
|
+
.superRefine((input, ctx) => {
|
|
234
|
+
// 非 pending 行きへの blocked_* 同梱はクライアントのバグの兆候なので黙殺せず 400 にする
|
|
235
|
+
if (input.status === 'pending')
|
|
236
|
+
return;
|
|
237
|
+
if (input.blocked_type != null) {
|
|
238
|
+
ctx.addIssue({
|
|
239
|
+
code: z.ZodIssueCode.custom,
|
|
240
|
+
path: ['blocked_type'],
|
|
241
|
+
message: 'pending 以外への移動では blocked_type を指定しない',
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
if (input.blocked_reason != null) {
|
|
245
|
+
ctx.addIssue({
|
|
246
|
+
code: z.ZodIssueCode.custom,
|
|
247
|
+
path: ['blocked_reason'],
|
|
248
|
+
message: 'pending 以外への移動では blocked_reason を指定しない',
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
/** PUT /api/tasks/:id の入力 (本文を含む全体更新。mtime 厳密比較で競合検知) */
|
|
253
|
+
export const saveInputSchema = z
|
|
254
|
+
.object({
|
|
255
|
+
content: z.string().min(1),
|
|
256
|
+
baseMtimeMs: z.number(),
|
|
257
|
+
})
|
|
258
|
+
.strict();
|
|
259
|
+
/** PUT が 409 のとき返す競合ボディ (クライアントは差分提示 → 上書き or 再読込) */
|
|
260
|
+
export const conflictBodySchema = z.object({
|
|
261
|
+
error: z.string(),
|
|
262
|
+
serverContent: z.string(),
|
|
263
|
+
serverMtimeMs: z.number(),
|
|
264
|
+
});
|
|
265
|
+
/** SSE で配信するイベント (通知のみ。クライアントは GET /api/board を再取得する) */
|
|
266
|
+
export const changeEventSchema = z.object({
|
|
267
|
+
type: z.enum(['connected', 'change']),
|
|
268
|
+
});
|
|
269
|
+
// ---------------------------------------------------------------------------
|
|
270
|
+
// overview モード (波 VII): 複数 .kanban の read-only 集約。GET /api/overview の応答型
|
|
271
|
+
// ---------------------------------------------------------------------------
|
|
272
|
+
/** ステータス別件数 (全ステータスを必須キーで持つ固定形) */
|
|
273
|
+
export const overviewStatusCountsSchema = z.object({
|
|
274
|
+
proposed: z.number().int().nonnegative(),
|
|
275
|
+
backlog: z.number().int().nonnegative(),
|
|
276
|
+
todo: z.number().int().nonnegative(),
|
|
277
|
+
in_progress: z.number().int().nonnegative(),
|
|
278
|
+
pending: z.number().int().nonnegative(),
|
|
279
|
+
done: z.number().int().nonnegative(),
|
|
280
|
+
});
|
|
281
|
+
/** プロジェクト 1 件のサマリ */
|
|
282
|
+
export const overviewProjectSchema = z.object({
|
|
283
|
+
/** 表示名 (config.projectName。config 不在 / 破損時は dir の basename) */
|
|
284
|
+
name: z.string().min(1),
|
|
285
|
+
/** 表示用ディレクトリ名 (basename、同名ルートは連番付与で一意化) */
|
|
286
|
+
dir: z.string().min(1),
|
|
287
|
+
/** 解決済みルートのフルパス (同名 basename の判別用。ローカル限定ツールのため露出可・R12-10) */
|
|
288
|
+
root: z.string().min(1),
|
|
289
|
+
statusCounts: overviewStatusCountsSchema,
|
|
290
|
+
/** loadBoard の warnings 件数 */
|
|
291
|
+
warningCount: z.number().int().nonnegative(),
|
|
292
|
+
/** いずれかのカラムで wipLimit 超過があるか */
|
|
293
|
+
wipExceeded: z.boolean(),
|
|
294
|
+
});
|
|
295
|
+
/** 注目タスク 1 件 (in_progress / pending / proposed の横断テーブル用。本文は含めない) */
|
|
296
|
+
export const overviewTaskSchema = z.object({
|
|
297
|
+
id: z.string().min(1),
|
|
298
|
+
title: z.string(),
|
|
299
|
+
status: z.enum(['in_progress', 'pending', 'proposed']),
|
|
300
|
+
assignee: z.string().nullable(),
|
|
301
|
+
blocked_type: z.enum(BLOCKED_TYPES).nullable(),
|
|
302
|
+
blocked_reason: z.string().nullable(),
|
|
303
|
+
/** projects[].dir への参照 (どの案件のタスクか) */
|
|
304
|
+
project: z.string().min(1),
|
|
305
|
+
});
|
|
306
|
+
/** GET /api/overview の応答 */
|
|
307
|
+
export const overviewResponseSchema = z.object({
|
|
308
|
+
projects: z.array(overviewProjectSchema),
|
|
309
|
+
attention: z.array(overviewTaskSchema),
|
|
310
|
+
});
|
|
311
|
+
/** GET /api/mode の応答 (SPA が起動時に 1 回呼んで board / overview を分岐する) */
|
|
312
|
+
export const modeResponseSchema = z.object({
|
|
313
|
+
mode: z.enum(['board', 'overview']),
|
|
314
|
+
});
|
|
315
|
+
/** POST /api/tasks/:id/reject の入力 (却下理由。作業ログに 1 行で追記される) */
|
|
316
|
+
export const rejectInputSchema = z
|
|
317
|
+
.object({
|
|
318
|
+
reason: singleLineString,
|
|
319
|
+
})
|
|
320
|
+
.strict();
|
|
321
|
+
// ---------------------------------------------------------------------------
|
|
322
|
+
// AI 設定ファイル管理 (F-4) の境界型
|
|
323
|
+
// ---------------------------------------------------------------------------
|
|
324
|
+
/** GET /api/files?path=... のレスポンス (許可リスト内ファイルの内容) */
|
|
325
|
+
export const fileContentSchema = z.object({
|
|
326
|
+
path: z.string(),
|
|
327
|
+
content: z.string(),
|
|
328
|
+
mtimeMs: z.number(),
|
|
329
|
+
});
|
|
330
|
+
/** PUT /api/files の入力 (許可リスト内ファイルの保存。mtime 競合検知) */
|
|
331
|
+
export const fileSaveInputSchema = z
|
|
332
|
+
.object({
|
|
333
|
+
path: z.string().min(1),
|
|
334
|
+
content: z.string(),
|
|
335
|
+
baseMtimeMs: z.number(),
|
|
336
|
+
})
|
|
337
|
+
.strict();
|
|
338
|
+
/** カタログ項目 (skill / agent の frontmatter name/description 抽出。F-4-4) */
|
|
339
|
+
export const catalogItemSchema = z.object({
|
|
340
|
+
path: z.string(),
|
|
341
|
+
name: z.string().nullable(),
|
|
342
|
+
description: z.string().nullable(),
|
|
343
|
+
});
|
|
344
|
+
/**
|
|
345
|
+
* git 履歴連携 (S-1) のコミット参照。GET /api/tasks/:id/commits が返す要素型。
|
|
346
|
+
* hash = フル SHA / subject = コミット件名 (1 行目) / authorDate = ISO 8601 文字列。
|
|
347
|
+
* いずれも git log の公開情報。server (git.ts) と web (TaskDetailPanel) で型共有する。
|
|
348
|
+
*/
|
|
349
|
+
export const commitRefSchema = z.object({
|
|
350
|
+
hash: z.string(),
|
|
351
|
+
subject: z.string(),
|
|
352
|
+
authorDate: z.string(),
|
|
353
|
+
});
|
|
354
|
+
/** POST /api/tasks/:id/comment の入力 (タスクコメント・往復。本文 ## やりとり に追記) */
|
|
355
|
+
export const commentInputSchema = z
|
|
356
|
+
.object({
|
|
357
|
+
member: z.string().min(1),
|
|
358
|
+
/** 1 行コメント (作業ログと同形式で追記する) */
|
|
359
|
+
message: singleLineString,
|
|
360
|
+
})
|
|
361
|
+
.strict();
|
|
362
|
+
/** POST /api/tasks/:id/rename の入力 (重複 ID リネーム支援 F-6-5。空き番号へ採番し直す) */
|
|
363
|
+
export const renameInputSchema = z.object({}).strict();
|
|
364
|
+
/**
|
|
365
|
+
* 新規タスクの本文テンプレート (要件 §5.3 のファイル例のセクション構成)。
|
|
366
|
+
* HTTP ルート (app.ts) と AI 用 CLI (task-ops.ts) の起票が同じ本文を書くよう、唯一の定義をここに置く。
|
|
367
|
+
*/
|
|
368
|
+
export const NEW_TASK_BODY = '\n## 概要\n\n\n## 受け入れ条件\n\n\n## 作業ログ\n';
|
|
369
|
+
/**
|
|
370
|
+
* zod エラーを `フィールド名: 制約` 形式の配列に変換する (HTTP / CLI / board 警告で共有)。
|
|
371
|
+
* N-8: ファイル本文・設定内容は含めず、パスとメッセージだけを返す。
|
|
372
|
+
* 文字列が必要な箇所 (board の警告) は呼び出し側で `.join(' / ')` する。
|
|
373
|
+
*/
|
|
374
|
+
export function summarizeZodError(error) {
|
|
375
|
+
return error.issues.map((issue) => `${issue.path.join('.') || '(root)'}: ${issue.message}`);
|
|
376
|
+
}
|