aizuchi 0.1.0 → 0.2.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/README.md +5 -3
- package/dist/server/app.js +79 -2
- package/dist/server/lib/pinpoint.js +50 -0
- package/dist/shared/schema.js +43 -10
- package/dist/web/assets/index-C15nwft7.js +82 -0
- package/dist/web/assets/index-qSD20HmA.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/templates/config.json +6 -6
- package/dist/web/assets/index-BLN_aXBe.css +0 -1
- package/dist/web/assets/index-BS_n8d9B.js +0 -82
package/README.md
CHANGED
|
@@ -43,6 +43,8 @@ npx aizuchi overview <dir...> # 例: npx aizuchi overview ~/proj-a ~/proj-b
|
|
|
43
43
|
|
|
44
44
|
- **カンバンボード**: config の `columns` どおりにカラム表示。カードに ID・タイトル・担当(human/ai の種別を
|
|
45
45
|
色で区別)・優先度・ラベル・最終更新からの経過時間を表示。
|
|
46
|
+
カラム名は `columns[].label` が無ければ UI の表示言語に追随します(`label` を書けばカスタム名として固定。
|
|
47
|
+
既存の config で日本語表示にしたい場合は `label` 行を削除してください)。
|
|
46
48
|
- **ドラッグ&ドロップ**: カラム移動で frontmatter の `status` / `updated` だけをピンポイント書換(本文は保全)。
|
|
47
49
|
Pending へ移すと種別(decision / external / dependency)と理由を尋ねるダイアログが出ます。
|
|
48
50
|
- **ライブ反映**: エディタや AI がファイルを変更すると、watcher → SSE で開いているボードへ約 1 秒以内に反映。
|
|
@@ -106,7 +108,7 @@ aizuchi/
|
|
|
106
108
|
- 設定は `.kanban/config.json`(プロジェクト名・ID 接頭辞・ポート・メンバー・カラム ほか)。
|
|
107
109
|
設定ゼロでも既定値で動作します。
|
|
108
110
|
|
|
109
|
-
|
|
111
|
+
各フィールドの enum 値・必須条件など完全な frontmatter スキーマは、`aizuchi init` が `CLAUDE.md` に追記する運用規約(frontmatter スキーマ表)に記載されます。init 後すぐ手元で確認できます。
|
|
110
112
|
|
|
111
113
|
## 手動確認チェックリスト(受け入れ基準 / DoD)
|
|
112
114
|
|
|
@@ -135,14 +137,14 @@ aizuchi/
|
|
|
135
137
|
Node.js 20.1+ / Hono + @hono/node-server / chokidar / SSE / React 18 + TypeScript + Vite /
|
|
136
138
|
@dnd-kit / react-markdown + remark-gfm / gray-matter / zod / CSS Modules / vitest。
|
|
137
139
|
|
|
138
|
-
###
|
|
140
|
+
### 主要スタックに追加した依存とその理由
|
|
139
141
|
|
|
140
142
|
- **@hono/node-server** — Hono を Node ランタイムで起動し、ビルド済み SPA を静的配信するため
|
|
141
143
|
- **js-yaml (+ @types/js-yaml)** — gray-matter に `JSON_SCHEMA` エンジンを注入し、`+09:00` 付き日時を Date 化せず文字列のまま往復させるため(gray-matter 内蔵の旧 3.x に transitive 依存しない)
|
|
142
144
|
- **@vitejs/plugin-react** — Vite で React(JSX / Fast Refresh)を使うため
|
|
143
145
|
- **@dnd-kit/utilities** — @dnd-kit ファミリ。D&D の CSS transform 補助(sortable はカラム内並べ替えが入る Phase 2 で追加)
|
|
144
146
|
- **tsx** — 開発時に TypeScript サーバーを直接実行するため(本番は dist のコンパイル済み JS を使用)
|
|
145
|
-
- **typescript / vitest / @types/\*** —
|
|
147
|
+
- **typescript / vitest / @types/\*** — 型安全とテストの基盤
|
|
146
148
|
|
|
147
149
|
## ライセンス
|
|
148
150
|
|
package/dist/server/app.js
CHANGED
|
@@ -4,7 +4,7 @@ import { Hono } from 'hono';
|
|
|
4
4
|
import { bodyLimit } from 'hono/body-limit';
|
|
5
5
|
import { HTTPException } from 'hono/http-exception';
|
|
6
6
|
import { streamSSE } from 'hono/streaming';
|
|
7
|
-
import { commentInputSchema, configSchema, fileSaveInputSchema, renameInputSchema, findMemberViolations, moveInputSchema, newTaskInputSchema, NEW_TASK_BODY, rejectInputSchema, saveInputSchema, summarizeZodError, taskFrontmatterSchema, taskIdSchema, } from '../shared/schema.js';
|
|
7
|
+
import { commentInputSchema, configSchema, fileSaveInputSchema, memberRenameInputSchema, renameInputSchema, findMemberViolations, moveInputSchema, newTaskInputSchema, NEW_TASK_BODY, rejectInputSchema, saveInputSchema, summarizeZodError, taskFrontmatterSchema, taskIdSchema, } from '../shared/schema.js';
|
|
8
8
|
import { loadArchive, loadBoard, loadConfig, loadTaskView } from './board.js';
|
|
9
9
|
import { createEventBus } from './events.js';
|
|
10
10
|
import { allocateNextId, createSerialQueue } from './lib/id.js';
|
|
@@ -12,7 +12,7 @@ import { assertDirInsideKanban, FileExistsError, PathValidationError, readKanban
|
|
|
12
12
|
import { buildDocsTree, readDoc } from './lib/docs.js';
|
|
13
13
|
import { findCommitsForTask } from './lib/git.js';
|
|
14
14
|
import { buildCatalog, buildFileTree, fileMtime, readConfigFile, saveConfigFile } from './lib/files.js';
|
|
15
|
-
import { applyPinpointPatch, renameTaskId, replaceDependsOnId, verifyPinpointResult, verifyRename, } from './lib/pinpoint.js';
|
|
15
|
+
import { applyPinpointPatch, renameTaskId, replaceDependsOnId, replaceMemberKeys, verifyMemberRename, verifyPinpointResult, verifyRename, } from './lib/pinpoint.js';
|
|
16
16
|
import { appendCommentLine, appendWorkLogLine, replaceBody } from './lib/proposal.js';
|
|
17
17
|
import { parseTaskFile, serializeTaskFile, TaskFileParseError } from './lib/task-file.js';
|
|
18
18
|
import { archiveMonth, nowIso, workLogTimestamp } from './lib/time.js';
|
|
@@ -578,6 +578,83 @@ export function createApp(options) {
|
|
|
578
578
|
});
|
|
579
579
|
});
|
|
580
580
|
// ---------------------------------------------------------------------
|
|
581
|
+
// POST /api/members/rename — member スラッグ変更 / 削除時の移譲。
|
|
582
|
+
// 全タスク (tasks/ のみ。archive は歴史として触らない) の created_by / assignee の
|
|
583
|
+
// from を to へ付け替える。作業ログ内の [member] 表記も歴史なので書き換えない。
|
|
584
|
+
// updated は更新しない (メタデータ移行であり経過時間表示を汚さない)。
|
|
585
|
+
// ---------------------------------------------------------------------
|
|
586
|
+
app.post('/api/members/rename', async (c) => {
|
|
587
|
+
const input = memberRenameInputSchema.safeParse(await c.req.json().catch(() => ({})));
|
|
588
|
+
if (!input.success) {
|
|
589
|
+
return c.json({ error: '入力が不正', issues: summarizeZodError(input.error) }, 400);
|
|
590
|
+
}
|
|
591
|
+
const { from, to } = input.data;
|
|
592
|
+
return enqueue(async () => {
|
|
593
|
+
const kanbanReal = await getKanbanReal(); // ループ前に realpath 信頼境界を一度確立 (F-6-5 連鎖更新と同じ)
|
|
594
|
+
let names = [];
|
|
595
|
+
try {
|
|
596
|
+
names = await fs.readdir(tasksDir);
|
|
597
|
+
}
|
|
598
|
+
catch {
|
|
599
|
+
names = [];
|
|
600
|
+
}
|
|
601
|
+
let renamed = 0;
|
|
602
|
+
const skipped = [];
|
|
603
|
+
for (const name of names) {
|
|
604
|
+
if (!name.endsWith('.md'))
|
|
605
|
+
continue;
|
|
606
|
+
const p = path.join(tasksDir, name);
|
|
607
|
+
let content;
|
|
608
|
+
try {
|
|
609
|
+
content = await readKanbanFile(p, kanbanReal); // containment + symlink 非追従 (symlink は対象外)
|
|
610
|
+
}
|
|
611
|
+
catch {
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
let before;
|
|
615
|
+
try {
|
|
616
|
+
before = parseTaskFile(content);
|
|
617
|
+
}
|
|
618
|
+
catch {
|
|
619
|
+
continue; // 破損ファイルは対象外 (既存の invalid_task 警告に任せる)
|
|
620
|
+
}
|
|
621
|
+
const keys = [];
|
|
622
|
+
if (before.data.created_by === from)
|
|
623
|
+
keys.push('created_by');
|
|
624
|
+
if (before.data.assignee === from)
|
|
625
|
+
keys.push('assignee');
|
|
626
|
+
if (keys.length === 0)
|
|
627
|
+
continue;
|
|
628
|
+
const patched = replaceMemberKeys(content, keys, to);
|
|
629
|
+
let after;
|
|
630
|
+
try {
|
|
631
|
+
after = parseTaskFile(patched);
|
|
632
|
+
}
|
|
633
|
+
catch {
|
|
634
|
+
skipped.push(name);
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
// verify-after-patch: 対象キーだけが to に変わり他フィールド/本文が不変かを書込前に検証。
|
|
638
|
+
// 未対応表記は書き換えずスキップし、応答で列挙する (silent な取りこぼしを可視化)
|
|
639
|
+
const drift = verifyMemberRename(before, after, keys, to);
|
|
640
|
+
if (drift !== null) {
|
|
641
|
+
skipped.push(name);
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
// 書込失敗 (fs エラー等) も全体を中断させず skipped に積んで続行する —
|
|
645
|
+
// 途中失敗で renamed/skipped の進捗が失われると部分適用の把握ができなくなる
|
|
646
|
+
try {
|
|
647
|
+
await safeWriteFile(p, patched, [kanbanDir]);
|
|
648
|
+
renamed += 1;
|
|
649
|
+
}
|
|
650
|
+
catch {
|
|
651
|
+
skipped.push(name);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
return c.json({ renamed, skipped });
|
|
655
|
+
});
|
|
656
|
+
});
|
|
657
|
+
// ---------------------------------------------------------------------
|
|
581
658
|
// GET /api/config — 生 config JSON を返す (設定画面 F-5 の編集元)
|
|
582
659
|
// ---------------------------------------------------------------------
|
|
583
660
|
app.get('/api/config', async (c) => {
|
|
@@ -137,6 +137,56 @@ export function replaceDependsOnId(raw, oldId, newId) {
|
|
|
137
137
|
}
|
|
138
138
|
return changed ? bom + lines.join('\n') : raw;
|
|
139
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* member 移譲: 指定キーの frontmatter 行を `key: <to>` に置き換える (本文・他フィールドは保全)。
|
|
142
|
+
* どのキーを書き換えるかは呼び出し側が parse 済みの値 (=== from) で判定する (行置換は値を読まない)。
|
|
143
|
+
* 対象行が見つからない場合はそのキーを変更しない — 呼び出し側の verifyMemberRename が不一致を検出し
|
|
144
|
+
* 書込を拒否する (壊れた表記で silent に取りこぼさない)
|
|
145
|
+
*/
|
|
146
|
+
export function replaceMemberKeys(raw, keys, to) {
|
|
147
|
+
const bom = raw.startsWith('\uFEFF') ? '\uFEFF' : '';
|
|
148
|
+
const text = bom ? raw.slice(1) : raw;
|
|
149
|
+
const fm = frontmatterRange(text);
|
|
150
|
+
if (!fm)
|
|
151
|
+
return raw;
|
|
152
|
+
const { lines, close } = fm;
|
|
153
|
+
for (const key of keys) {
|
|
154
|
+
for (let i = 1; i < close; i++) {
|
|
155
|
+
const line = (lines[i] ?? '').replace(/\r$/, '');
|
|
156
|
+
if (line === `${key}:` || line.startsWith(`${key}: `) || line.startsWith(`${key}:\t`)) {
|
|
157
|
+
const rendered = `${key}: ${dumpScalar(to)}`;
|
|
158
|
+
lines[i] = (lines[i] ?? '').endsWith('\r') ? `${rendered}\r` : rendered;
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return bom + lines.join('\n');
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* verify-after-member-rename ガード (verifyPinpointResult と同じ思想)。
|
|
167
|
+
* 「指定キーだけが to に変わり、他フィールドと本文は不変」を patch 前後の再 parse で検証する。
|
|
168
|
+
* 不一致なら理由文字列 (呼び出し側で書込を拒否)、問題なければ null。
|
|
169
|
+
*/
|
|
170
|
+
export function verifyMemberRename(before, after, keys, to) {
|
|
171
|
+
const keySet = new Set(keys);
|
|
172
|
+
for (const key of keys) {
|
|
173
|
+
if (after.data[key] !== to) {
|
|
174
|
+
return `行置換が ${key} を正しく書き換えられなかった (この表記はピンポイント書換に未対応)`;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const allKeys = new Set([...Object.keys(before.data), ...Object.keys(after.data)]);
|
|
178
|
+
for (const key of allKeys) {
|
|
179
|
+
if (keySet.has(key))
|
|
180
|
+
continue;
|
|
181
|
+
if (!valuesEqual(before.data[key], after.data[key])) {
|
|
182
|
+
return `行置換が意図しないフィールド ${key} を変更した (この表記はピンポイント書換に未対応)`;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (before.body !== after.body) {
|
|
186
|
+
return '行置換が本文を変更した (この表記はピンポイント書換に未対応)';
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
140
190
|
/**
|
|
141
191
|
* verify-after-patch ガード (PM Round 1 #2/#3/#6 への包括対処)。
|
|
142
192
|
*
|
package/dist/shared/schema.js
CHANGED
|
@@ -10,6 +10,8 @@ export const PRIORITIES = ['high', 'medium', 'low'];
|
|
|
10
10
|
export const MEMBER_TYPES = ['human', 'ai'];
|
|
11
11
|
// マイルストーンの状態 (Phase 2 波 II の新規スキーマ拡張)
|
|
12
12
|
export const MILESTONE_STATUSES = ['planned', 'active', 'done'];
|
|
13
|
+
// UI 表示言語。web の i18n (LANGS) と config.language が共有する唯一の源
|
|
14
|
+
export const UI_LANGS = ['en', 'ja'];
|
|
13
15
|
export const DEFAULT_PORT = 5151;
|
|
14
16
|
/** overview モードの既定ポート (波 VII)。serve の 5151 と衝突しない値・1 箇所集約 */
|
|
15
17
|
export const DEFAULT_OVERVIEW_PORT = 5152;
|
|
@@ -26,12 +28,17 @@ export const taskIdSchema = z
|
|
|
26
28
|
// 1 行テキスト (title 等。改行を含むと行ベースの frontmatter 処理が壊れる)
|
|
27
29
|
const singleLineString = z.string().min(1).regex(/^[^\r\n]+$/, '1 行で書く (改行不可)');
|
|
28
30
|
export const memberSchema = z.object({
|
|
31
|
+
// スラッグ (タスクファイルの created_by / assignee に書かれる安定 ID)
|
|
29
32
|
id: z.string().min(1),
|
|
33
|
+
// 表示名 (任意)。未指定は id を表示 (columns label と同じパターン)。表示名の変更は id に影響しない
|
|
34
|
+
label: z.string().min(1).optional(),
|
|
30
35
|
type: z.enum(MEMBER_TYPES),
|
|
31
36
|
});
|
|
32
37
|
export const columnSchema = z.object({
|
|
33
38
|
id: z.string().min(1),
|
|
34
|
-
|
|
39
|
+
// 表示名 (任意)。未指定ならビュー層が表示言語に応じた status 訳を出す。
|
|
40
|
+
// 指定した場合はカスタム名としてどの言語でも最優先で表示される (後方互換)
|
|
41
|
+
label: z.string().min(1).optional(),
|
|
35
42
|
hideWhenEmpty: z.boolean().optional(),
|
|
36
43
|
wipLimit: z.number().int().positive().optional(),
|
|
37
44
|
});
|
|
@@ -65,25 +72,40 @@ export const configSchema = z.object({
|
|
|
65
72
|
.default('TASK'),
|
|
66
73
|
port: z.number().int().min(1).max(65535).default(DEFAULT_PORT),
|
|
67
74
|
aiTaskCreation: z.enum(['free', 'proposal']).default('free'),
|
|
75
|
+
// UI 表示言語 (任意)。localStorage は origin (port) 単位で分断されるため、ファイル側を正として言語を運ぶ。
|
|
76
|
+
// 未指定はクライアント既定 (localStorage → en) に任せる = 既存 config と後方互換
|
|
77
|
+
language: z.enum(UI_LANGS).optional(),
|
|
68
78
|
members: z
|
|
69
79
|
.array(memberSchema)
|
|
70
80
|
.min(1)
|
|
81
|
+
// 既定 member はテンプレート (templates/config.json) と同じ汎用名 (公開物に実名を載せない方針)
|
|
71
82
|
.default([
|
|
72
|
-
{ id: '
|
|
73
|
-
{ id: '
|
|
83
|
+
{ id: 'member1', type: 'human' },
|
|
84
|
+
{ id: 'member2', type: 'human' },
|
|
74
85
|
{ id: 'claude', type: 'ai' },
|
|
75
86
|
{ id: 'codex', type: 'ai' },
|
|
76
|
-
])
|
|
87
|
+
])
|
|
88
|
+
// id (スラッグ) はタスク参照の照合キーなので一意必須 (リネーム統合や手編集での重複混入を弾く)
|
|
89
|
+
.superRefine((members, ctx) => {
|
|
90
|
+
const seen = new Set();
|
|
91
|
+
for (const member of members) {
|
|
92
|
+
if (seen.has(member.id)) {
|
|
93
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: `member id が重複しています: ${member.id}` });
|
|
94
|
+
}
|
|
95
|
+
seen.add(member.id);
|
|
96
|
+
}
|
|
97
|
+
}),
|
|
98
|
+
// 既定カラムは label 無し = UI の表示言語に応じた status 訳で表示する
|
|
77
99
|
columns: z
|
|
78
100
|
.array(columnSchema)
|
|
79
101
|
.min(1)
|
|
80
102
|
.default([
|
|
81
|
-
{ id: 'proposed',
|
|
82
|
-
{ id: 'backlog'
|
|
83
|
-
{ id: 'todo'
|
|
84
|
-
{ id: 'in_progress',
|
|
85
|
-
{ id: 'pending'
|
|
86
|
-
{ id: 'done'
|
|
103
|
+
{ id: 'proposed', hideWhenEmpty: true },
|
|
104
|
+
{ id: 'backlog' },
|
|
105
|
+
{ id: 'todo' },
|
|
106
|
+
{ id: 'in_progress', wipLimit: 2 },
|
|
107
|
+
{ id: 'pending' },
|
|
108
|
+
{ id: 'done' },
|
|
87
109
|
]),
|
|
88
110
|
instructionTemplate: z
|
|
89
111
|
.string()
|
|
@@ -361,6 +383,17 @@ export const commentInputSchema = z
|
|
|
361
383
|
.strict();
|
|
362
384
|
/** POST /api/tasks/:id/rename の入力 (重複 ID リネーム支援 F-6-5。空き番号へ採番し直す) */
|
|
363
385
|
export const renameInputSchema = z.object({}).strict();
|
|
386
|
+
/**
|
|
387
|
+
* POST /api/members/rename の入力 (member スラッグ変更 / 削除時の移譲)。
|
|
388
|
+
* 全タスクの created_by / assignee の from を to へ付け替える
|
|
389
|
+
*/
|
|
390
|
+
export const memberRenameInputSchema = z
|
|
391
|
+
.object({
|
|
392
|
+
from: singleLineString,
|
|
393
|
+
to: singleLineString,
|
|
394
|
+
})
|
|
395
|
+
.strict()
|
|
396
|
+
.refine((v) => v.from !== v.to, { message: 'from と to が同じです' });
|
|
364
397
|
/**
|
|
365
398
|
* 新規タスクの本文テンプレート (要件 §5.3 のファイル例のセクション構成)。
|
|
366
399
|
* HTTP ルート (app.ts) と AI 用 CLI (task-ops.ts) の起票が同じ本文を書くよう、唯一の定義をここに置く。
|