aizuchi 0.2.0 → 0.4.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/CHANGELOG.md +147 -0
- package/README.md +77 -17
- package/dist/bin/aizuchi.js +120 -2
- package/dist/server/app.js +314 -9
- package/dist/server/board.js +35 -5
- package/dist/server/lib/config-paths.js +60 -19
- package/dist/server/lib/files.js +56 -21
- package/dist/server/lib/proposal.js +10 -0
- package/dist/server/lib/task-ops.js +102 -2
- package/dist/server/lib/trash.js +122 -0
- package/dist/server/watcher.js +17 -4
- package/dist/shared/board-diff.js +48 -0
- package/dist/shared/diff.js +16 -0
- package/dist/shared/schema.js +82 -0
- package/dist/shared/stats.js +117 -1
- package/dist/web/assets/index-BYfu8Hdw.js +93 -0
- package/dist/web/assets/index-DiMXsTSP.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +3 -2
- package/templates/config.json +2 -1
- package/dist/web/assets/index-C15nwft7.js +0 -82
- package/dist/web/assets/index-qSD20HmA.css +0 -1
package/dist/shared/schema.js
CHANGED
|
@@ -64,6 +64,22 @@ export const taskTemplateSchema = z
|
|
|
64
64
|
priority: z.enum(PRIORITIES).optional(),
|
|
65
65
|
})
|
|
66
66
|
.strict();
|
|
67
|
+
/**
|
|
68
|
+
* 保存済みビュー定義 (絞り込みプリセット)。よく使うボードの絞り込み (担当 / ラベル / 検索語 /
|
|
69
|
+
* status) を config に名前付きで保存し、ワンクリックで適用する。
|
|
70
|
+
* language と同じく config.json を正とする (localStorage は origin=port 単位で分断され git 同期もされない)。
|
|
71
|
+
* name は chip のラベル (必須)、query/label/assignee/statuses は適用する絞り込み値 (任意)。
|
|
72
|
+
* .strict で未知フィールドを拒否する (config の typo を可視化)。既定は空配列で後方互換。
|
|
73
|
+
*/
|
|
74
|
+
export const savedViewSchema = z
|
|
75
|
+
.object({
|
|
76
|
+
name: z.string().min(1),
|
|
77
|
+
query: z.string().optional(),
|
|
78
|
+
label: z.string().optional(),
|
|
79
|
+
assignee: z.string().optional(),
|
|
80
|
+
statuses: z.array(z.enum(TASK_STATUSES)).optional(),
|
|
81
|
+
})
|
|
82
|
+
.strict();
|
|
67
83
|
export const configSchema = z.object({
|
|
68
84
|
projectName: z.string().min(1).default('project'),
|
|
69
85
|
idPrefix: z
|
|
@@ -121,8 +137,29 @@ export const configSchema = z.object({
|
|
|
121
137
|
milestones: z.array(milestoneSchema).default([]),
|
|
122
138
|
// タスクテンプレート定義 (波 VI)。既定は空 = 既存 config と後方互換 (テンプレート UI は非表示)
|
|
123
139
|
taskTemplates: z.array(taskTemplateSchema).default([]),
|
|
140
|
+
// 保存済みビュー (絞り込みプリセット)。既定は空 = 既存 config と後方互換 (chip 非表示・N-6)。
|
|
141
|
+
// language と同じくファイル側を正とし、ポート差・案件横断で効き git 同期される (localStorage 不使用)
|
|
142
|
+
savedViews: z.array(savedViewSchema).default([]),
|
|
143
|
+
// ビュー切替タブの表示順 + 表示/非表示。順序付き {id, hidden?}。既定は空 = 既定順で全表示 (後方互換)。
|
|
144
|
+
// id は web 側の既定ビュー集合 (board/critical/...) と突き合わせ、未知 id は無視・未列挙の既定ビューは末尾表示。
|
|
145
|
+
views: z.array(z.object({ id: z.string().min(1), hidden: z.boolean().optional() })).default([]),
|
|
124
146
|
// 関連資料ディレクトリ (波 VIII)。プロジェクトルートからの相対パス。既定は空 = 機能オフ (UI 非表示)
|
|
125
147
|
docsDirs: z.array(z.string().min(1)).default([]),
|
|
148
|
+
// ごみ箱 (.kanban/.trash) の同 id あたり退避世代の保持件数。任意・後方互換 (未設定は DEFAULT_TRASH_KEEP)。
|
|
149
|
+
// 破壊的書込の直前に旧版を .trash へコピー退避し、この件数を超えた古い世代を刈り取る (誤上書きの安全網)
|
|
150
|
+
trashKeep: z.number().int().positive().optional(),
|
|
151
|
+
// 停滞アラート閾値 (静かなタスク検知)。status 別に「最後の作業ログから何日で停滞とみなすか」(単位: 日)。
|
|
152
|
+
// 任意・後方互換 (milestones/docsDirs 前例): 未設定でも DEFAULT_STALL_THRESHOLDS で動く (N-6)。
|
|
153
|
+
// partial なので個別 status だけ上書き可。done/archive は対象外なので持たない
|
|
154
|
+
stallThresholds: z
|
|
155
|
+
.object({
|
|
156
|
+
in_progress: z.number().int().positive(),
|
|
157
|
+
todo: z.number().int().positive(),
|
|
158
|
+
pending: z.number().int().positive(),
|
|
159
|
+
})
|
|
160
|
+
.strict()
|
|
161
|
+
.partial()
|
|
162
|
+
.optional(),
|
|
126
163
|
});
|
|
127
164
|
/** 既定 config を生成する (N-6: 設定ゼロでも既定値で動作する) */
|
|
128
165
|
export function defaultConfig() {
|
|
@@ -340,6 +377,17 @@ export const rejectInputSchema = z
|
|
|
340
377
|
reason: singleLineString,
|
|
341
378
|
})
|
|
342
379
|
.strict();
|
|
380
|
+
/**
|
|
381
|
+
* POST /api/tasks/:id/revise の入力 (やり直し依頼。done/in_progress/pending から todo へ戻す)。
|
|
382
|
+
* reason は任意・改行可 (差し戻しの説明が複数行になりうるため reject の 1 行制約とは別扱い)。
|
|
383
|
+
* baseMtimeMs があれば PATCH/PUT と同じ mtime 競合検知に乗る (N-3)。
|
|
384
|
+
*/
|
|
385
|
+
export const reviseInputSchema = z
|
|
386
|
+
.object({
|
|
387
|
+
reason: z.string().max(2000).optional(),
|
|
388
|
+
baseMtimeMs: z.number().optional(),
|
|
389
|
+
})
|
|
390
|
+
.strict();
|
|
343
391
|
// ---------------------------------------------------------------------------
|
|
344
392
|
// AI 設定ファイル管理 (F-4) の境界型
|
|
345
393
|
// ---------------------------------------------------------------------------
|
|
@@ -355,6 +403,8 @@ export const fileSaveInputSchema = z
|
|
|
355
403
|
path: z.string().min(1),
|
|
356
404
|
content: z.string(),
|
|
357
405
|
baseMtimeMs: z.number(),
|
|
406
|
+
// 'global' = ~/.claude/skills,agents への保存 (省略 = プロジェクトローカル)。グローバルは UI で警告のうえ保存
|
|
407
|
+
scope: z.enum(['local', 'global']).optional(),
|
|
358
408
|
})
|
|
359
409
|
.strict();
|
|
360
410
|
/** カタログ項目 (skill / agent の frontmatter name/description 抽出。F-4-4) */
|
|
@@ -383,6 +433,38 @@ export const commentInputSchema = z
|
|
|
383
433
|
.strict();
|
|
384
434
|
/** POST /api/tasks/:id/rename の入力 (重複 ID リネーム支援 F-6-5。空き番号へ採番し直す) */
|
|
385
435
|
export const renameInputSchema = z.object({}).strict();
|
|
436
|
+
// ---------------------------------------------------------------------------
|
|
437
|
+
// ごみ箱 (.kanban/.trash) の境界型。破壊的書込の直前に旧版をコピー退避した一覧 + 復元
|
|
438
|
+
// ---------------------------------------------------------------------------
|
|
439
|
+
/** GET /api/trash の退避 1 件 (軽量メタ + プレビュー用本文。frontmatter parse 成功時のみ表示用 title) */
|
|
440
|
+
export const trashItemSchema = z.object({
|
|
441
|
+
/** 元タスク ID */
|
|
442
|
+
id: z.string().min(1),
|
|
443
|
+
/** 退避時刻のエポックミリ秒 (世代の識別子。restore の trashedAt に渡す) */
|
|
444
|
+
trashedAt: z.number().int().nonnegative(),
|
|
445
|
+
/** frontmatter が parse できれば title (壊れた退避では null) */
|
|
446
|
+
title: z.string().nullable(),
|
|
447
|
+
/** 退避時の status (frontmatter が zod 検証を通った時のみ。壊れた退避は null) */
|
|
448
|
+
status: z.enum(TASK_STATUSES).nullable(),
|
|
449
|
+
/** 退避内容の本文 (プレビュー用。frontmatter 以降。parse 不能なら全文) */
|
|
450
|
+
body: z.string(),
|
|
451
|
+
});
|
|
452
|
+
/** GET /api/trash の応答 (退避一覧。新しい退避が先頭) */
|
|
453
|
+
export const trashListResponseSchema = z.object({
|
|
454
|
+
items: z.array(trashItemSchema),
|
|
455
|
+
});
|
|
456
|
+
/**
|
|
457
|
+
* POST /api/tasks/:id/restore の入力 (ごみ箱からの復元)。
|
|
458
|
+
* trashedAt 省略時は当該 id の最新世代を復元する。空オブジェクト可 (最新復元)。
|
|
459
|
+
* overwrite が true なら tasks/<id>.md が既存でも上書き復元する (現行は退避してから上書き)。
|
|
460
|
+
* 未指定 / false なら既存時は 409 (上書きしない・unarchive と対称) のまま。
|
|
461
|
+
*/
|
|
462
|
+
export const restoreInputSchema = z
|
|
463
|
+
.object({
|
|
464
|
+
trashedAt: z.number().int().nonnegative().optional(),
|
|
465
|
+
overwrite: z.boolean().optional(),
|
|
466
|
+
})
|
|
467
|
+
.strict();
|
|
386
468
|
/**
|
|
387
469
|
* POST /api/members/rename の入力 (member スラッグ変更 / 削除時の移譲)。
|
|
388
470
|
* 全タスクの created_by / assignee の from を to へ付け替える
|
package/dist/shared/stats.js
CHANGED
|
@@ -9,6 +9,121 @@ import { parseWorkLogLines } from './digest.js';
|
|
|
9
9
|
import { TASK_STATUSES } from './schema.js';
|
|
10
10
|
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
11
11
|
const DEFAULT_MAX_BOTTLENECKS = 5;
|
|
12
|
+
/** 停滞アラートの対象ステータス (done/archive は対象外。能動的に進める必要がある 3 状態のみ) */
|
|
13
|
+
export const STALL_STATUSES = ['in_progress', 'todo', 'pending'];
|
|
14
|
+
/**
|
|
15
|
+
* 停滞アラートの既定閾値 (単位: 日)。設定ゼロでも動く既定値の唯一の集約点 (N-6・ハードコード回避)。
|
|
16
|
+
* in_progress は能動的に進行中のはずなので短く、todo は着手前で長め、pending は待ち中で中間。
|
|
17
|
+
*/
|
|
18
|
+
export const DEFAULT_STALL_THRESHOLDS = {
|
|
19
|
+
in_progress: 2,
|
|
20
|
+
todo: 7,
|
|
21
|
+
pending: 5,
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* 「人の判断待ち」(status='pending' かつ blocked_type='decision') のタスクを抽出する純関数。
|
|
25
|
+
* AI が blocked_type=decision で pending にしても人間が気づけない問題への read-only 派生表示。
|
|
26
|
+
* ID 昇順 (数値を考慮した自然順) で返す。新データ源・新依存・API 追加はゼロ。
|
|
27
|
+
*/
|
|
28
|
+
export function deriveDecisionWaits(tasks) {
|
|
29
|
+
return tasks
|
|
30
|
+
.filter((t) => t.status === 'pending' && t.blocked_type === 'decision')
|
|
31
|
+
.map((t) => ({
|
|
32
|
+
id: t.id,
|
|
33
|
+
title: t.title,
|
|
34
|
+
blocked_reason: t.blocked_reason,
|
|
35
|
+
assignee: t.assignee,
|
|
36
|
+
}))
|
|
37
|
+
.sort((a, b) => a.id.localeCompare(b.id, 'en', { numeric: true }));
|
|
38
|
+
}
|
|
39
|
+
/** status が停滞対象 (in_progress/todo/pending) かを型ガードで判定する */
|
|
40
|
+
function isStallStatus(status) {
|
|
41
|
+
return STALL_STATUSES.includes(status);
|
|
42
|
+
}
|
|
43
|
+
// `## 作業ログ` 見出し行の検出 (level 不問・前後空白許容)。見出し文言は §5.3 / CLAUDE.md テンプレートに準拠。
|
|
44
|
+
const WORKLOG_HEADING_RE = /^\s{0,3}#{1,6}\s+作業ログ\s*$/;
|
|
45
|
+
// 任意の Markdown 見出し行 (セクション境界の検出に使う)。
|
|
46
|
+
const ANY_HEADING_RE = /^\s{0,3}#{1,6}\s+/;
|
|
47
|
+
/**
|
|
48
|
+
* stats 用に「作業ログセクションだけ」を切り出す (digest は全セクションを拾う現挙動を維持し、本関数は使わない)。
|
|
49
|
+
* `## 作業ログ` 見出しがあればその配下〜次の見出し直前までを返す。見出しが無ければ本文全体を返す (後方互換)。
|
|
50
|
+
* 目的: `## やりとり` のコメント行 (`- HH:mm [member]` と同書式) を停滞・完了集計が拾わないようにする (R-review #3)。
|
|
51
|
+
*/
|
|
52
|
+
function workLogBody(body) {
|
|
53
|
+
const lines = body.split('\n');
|
|
54
|
+
let start = -1;
|
|
55
|
+
for (let i = 0; i < lines.length; i++) {
|
|
56
|
+
if (WORKLOG_HEADING_RE.test(lines[i])) {
|
|
57
|
+
start = i + 1;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (start === -1)
|
|
62
|
+
return body; // 作業ログ見出しが無ければ全体 (テスト fixture や旧形式との後方互換)
|
|
63
|
+
const out = [];
|
|
64
|
+
for (let i = start; i < lines.length; i++) {
|
|
65
|
+
if (ANY_HEADING_RE.test(lines[i]))
|
|
66
|
+
break; // 次の見出し (## やりとり 等) でセクション終了
|
|
67
|
+
out.push(lines[i]);
|
|
68
|
+
}
|
|
69
|
+
return out.join('\n');
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* タスクの「最後の活動時刻」を決める (epoch ms と ISO 文字列)。
|
|
73
|
+
* 優先順位: (1) 作業ログセクションの「最も新しい」日時 → 無ければ (2) frontmatter.updated。
|
|
74
|
+
* 出現位置の末尾ではなく時刻最大で選ぶ (手編集での過去日付追記やバックフィルで誤判定しないため、R-review #2)。
|
|
75
|
+
* 作業ログ時刻は offset 無し (ローカル解釈)。updated は offset 付き絶対時刻 (deriveStats と同じ既知近似)。
|
|
76
|
+
* いずれも parse 不能なら null。
|
|
77
|
+
*/
|
|
78
|
+
function lastActivity(task) {
|
|
79
|
+
const logs = parseWorkLogLines(workLogBody(task.body));
|
|
80
|
+
let best = null;
|
|
81
|
+
for (const log of logs) {
|
|
82
|
+
const iso = `${log.date}T${log.time}`;
|
|
83
|
+
const ms = Date.parse(iso);
|
|
84
|
+
if (Number.isNaN(ms))
|
|
85
|
+
continue;
|
|
86
|
+
if (best === null || ms > best.ms)
|
|
87
|
+
best = { ms, iso };
|
|
88
|
+
}
|
|
89
|
+
if (best !== null)
|
|
90
|
+
return best;
|
|
91
|
+
const ms = parseIsoMs(task.updated);
|
|
92
|
+
return ms === null ? null : { ms, iso: task.updated };
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* 「静かなタスク (停滞)」を検知する純関数 (既存ボトルネック = pending 滞留のみ とは直交)。
|
|
96
|
+
* in_progress/todo/pending のうち、最後の作業ログ (無ければ updated) からの経過日数が
|
|
97
|
+
* status 別閾値 (日) を超えたものを idleDays 降順 → ID 昇順で返す。done/archive は対象外。
|
|
98
|
+
*
|
|
99
|
+
* @param tasks 対象タスク (status 不問。内部で対象 status のみ評価する)
|
|
100
|
+
* @param now 現在時刻 (epoch ms)。経過日数の基準 (Date.now ハードコード禁止・テスト再現性)
|
|
101
|
+
* @param thresholds status 別閾値 (日)。未指定の status は DEFAULT_STALL_THRESHOLDS を使う (N-6)
|
|
102
|
+
*/
|
|
103
|
+
export function findStalledTasks(tasks, now, thresholds = {}) {
|
|
104
|
+
const result = [];
|
|
105
|
+
for (const task of tasks) {
|
|
106
|
+
if (!isStallStatus(task.status))
|
|
107
|
+
continue; // done/backlog/proposed/archive は対象外
|
|
108
|
+
const activity = lastActivity(task);
|
|
109
|
+
if (activity === null)
|
|
110
|
+
continue; // 日時が parse 不能なら判定不可 (誤検知を避けスキップ)
|
|
111
|
+
const limitDays = thresholds[task.status] ?? DEFAULT_STALL_THRESHOLDS[task.status];
|
|
112
|
+
const idleMs = now - activity.ms;
|
|
113
|
+
// 閾値「超過」のみ停滞とみなす (ちょうど閾値ぴったりは含めない)
|
|
114
|
+
if (idleMs <= limitDays * MS_PER_DAY)
|
|
115
|
+
continue;
|
|
116
|
+
result.push({
|
|
117
|
+
id: task.id,
|
|
118
|
+
title: task.title,
|
|
119
|
+
status: task.status,
|
|
120
|
+
lastActivityIso: activity.iso,
|
|
121
|
+
idleDays: toDays(idleMs),
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
// 停滞が深い順 → 同率は ID 昇順 (自然順)
|
|
125
|
+
return result.sort((a, b) => b.idleDays - a.idleDays || a.id.localeCompare(b.id, 'en', { numeric: true }));
|
|
126
|
+
}
|
|
12
127
|
// 完了記録の偽陽性を避けるための否定文脈ガード (R10-1)。これにヒットする行は完了記録とみなさない。
|
|
13
128
|
// 例: 「未完了の項目」「完了予定を 7/1」「まだ done していない」「isn't finished」
|
|
14
129
|
const COMPLETION_NEGATIVE_RE = /未完了|未完|完了予定|完了見込|完了せず|undone|not\s+(done|finished|closed)|n['’]t\s+(done|finished|closed)/i;
|
|
@@ -46,7 +161,8 @@ function isCompletionRecord(message) {
|
|
|
46
161
|
* 非 JST ブラウザではサイクルタイムが offset 差ぶんずれる (JST 運用なら実害小)。
|
|
47
162
|
*/
|
|
48
163
|
function completionMs(task) {
|
|
49
|
-
|
|
164
|
+
// `## やりとり` のコメント行に完了キーワードがあっても拾わないよう、作業ログセクションに限定する (R-review #3)。
|
|
165
|
+
const logs = parseWorkLogLines(workLogBody(task.body));
|
|
50
166
|
for (let i = logs.length - 1; i >= 0; i--) {
|
|
51
167
|
if (isCompletionRecord(logs[i].message)) {
|
|
52
168
|
const ms = Date.parse(`${logs[i].date}T${logs[i].time}`);
|