aizuchi 0.3.0 → 0.5.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 +65 -1
- package/README.md +4 -3
- package/dist/bin/aizuchi.js +160 -13
- package/dist/server/app.js +94 -2
- package/dist/server/lib/docs.js +175 -2
- package/dist/server/lib/related-docs-validate.js +37 -0
- package/dist/server/lib/task-ops.js +1 -0
- package/dist/shared/ai-health.js +78 -0
- package/dist/shared/schema.js +11 -0
- package/dist/shared/task-setters.js +132 -0
- package/dist/web/assets/index-C3mZwbO4.js +103 -0
- package/dist/web/assets/index-QsPhBKSP.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-B4RPR1mO.css +0 -1
- package/dist/web/assets/index-DG0paXSh.js +0 -93
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { isSecretPath, isStrictlyInside, PathValidationError, sanitizeConfigDirEntries, } from './fs-safe.js';
|
|
3
|
+
/**
|
|
4
|
+
* 資料からの起票 (波 VIII) で frontmatter に保存して良い related_docs パスかを検証する純ロジック。
|
|
5
|
+
*
|
|
6
|
+
* - 対象ディレクトリ = config.docsDirs ∪ config.docsWriteDirs を sanitizeConfigDirEntries で
|
|
7
|
+
* root 配下に正規化し、.kanban 配下 / 秘匿パスを除外したもの (docs.ts の effectiveDocsDirs と同思想)。
|
|
8
|
+
* - 各要素は「.md 拡張子」「root 配下の相対パス (絶対 / .. を弾く)」「許可ディレクトリ配下」
|
|
9
|
+
* 「秘匿パスでない」を満たす必要がある。1 つでも違反すれば PathValidationError を投げる。
|
|
10
|
+
* - 同期判定のみ (fs.realpath は呼ばない): これは「読取」ではなく「frontmatter に書いて良い参照値か」の
|
|
11
|
+
* 妥当性チェック。symlink / TOCTOU 防御は読取時 (docs.ts resolveDocPath / readFileNoFollow) が担保する。
|
|
12
|
+
*/
|
|
13
|
+
export function assertRelatedDocsAllowed(projectRoot, config, docs) {
|
|
14
|
+
if (docs.length === 0)
|
|
15
|
+
return;
|
|
16
|
+
const root = path.resolve(projectRoot);
|
|
17
|
+
const kanbanDir = path.join(root, '.kanban');
|
|
18
|
+
const allowedDirs = sanitizeConfigDirEntries(root, [...config.docsDirs, ...config.docsWriteDirs]).filter((dir) => dir !== kanbanDir && !isStrictlyInside(kanbanDir, dir) && !isSecretPath(dir));
|
|
19
|
+
for (const rel of docs) {
|
|
20
|
+
if (!rel.endsWith('.md')) {
|
|
21
|
+
throw new PathValidationError('related_docs は .md ファイルのみ指定できます');
|
|
22
|
+
}
|
|
23
|
+
if (path.isAbsolute(rel)) {
|
|
24
|
+
throw new PathValidationError('related_docs に絶対パスは指定できません');
|
|
25
|
+
}
|
|
26
|
+
const resolved = path.resolve(root, rel);
|
|
27
|
+
if (resolved !== root && !resolved.startsWith(root + path.sep)) {
|
|
28
|
+
throw new PathValidationError('related_docs にプロジェクトルート外のパスは指定できません');
|
|
29
|
+
}
|
|
30
|
+
if (isSecretPath(resolved)) {
|
|
31
|
+
throw new PathValidationError('related_docs に秘匿ファイルは指定できません');
|
|
32
|
+
}
|
|
33
|
+
if (!allowedDirs.some((dir) => isStrictlyInside(dir, resolved))) {
|
|
34
|
+
throw new PathValidationError('related_docs は docsDirs / docsWriteDirs 配下の資料のみ指定できます');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -100,6 +100,7 @@ export class TaskOps {
|
|
|
100
100
|
created_by: parsed.data.created_by,
|
|
101
101
|
priority: parsed.data.priority ?? 'medium',
|
|
102
102
|
labels: parsed.data.labels ?? [],
|
|
103
|
+
related_docs: parsed.data.related_docs,
|
|
103
104
|
blocked_type: null,
|
|
104
105
|
blocked_reason: null,
|
|
105
106
|
created: now,
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI オンボーディング診断 (TASK-023)。
|
|
3
|
+
* board の warnings を AI メンバー別に集計し、各 warning kind に「直し方」の説明を付けた
|
|
4
|
+
* 構造を返す純関数。新データ源・新依存・永続化なし。
|
|
5
|
+
*
|
|
6
|
+
* now は引数注入 (Date.now ハードコード禁止・テスト再現性)。
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* warnings を AI メンバー別・kind 別に group-by する。
|
|
10
|
+
*
|
|
11
|
+
* - warning が特定の AI メンバーに紐付くか判定: taskId が null でない場合はタスクのパスから
|
|
12
|
+
* 直接判定できないため、メンバー ID を message 内から照合する (unregistered_member / mode_violation)。
|
|
13
|
+
* それ以外の kind (invalid_task / duplicate_id / invalid_config) は AI に特定紐付けできないため
|
|
14
|
+
* 全 AI メンバーに共通の「設定上の問題」として集約する。
|
|
15
|
+
* - assignee / created_by 別の精細な帰属は taskView へのアクセスが必要で pure fn では難しいため、
|
|
16
|
+
* 警告の message 文字列に含まれる member id を照合する簡易戦略を取る。
|
|
17
|
+
*
|
|
18
|
+
* @param warnings board.warnings (GET /api/board の warnings)
|
|
19
|
+
* @param members config.members (全メンバー)
|
|
20
|
+
*/
|
|
21
|
+
export function deriveAiHealth(warnings, members) {
|
|
22
|
+
const aiMembers = members.filter((m) => m.type === 'ai');
|
|
23
|
+
if (aiMembers.length === 0) {
|
|
24
|
+
return { members: [], noAiMembers: true };
|
|
25
|
+
}
|
|
26
|
+
const results = aiMembers.map((member) => {
|
|
27
|
+
// このメンバーに紐付く warning を収集する
|
|
28
|
+
const memberWarnings = warnings.filter((w) => isWarningForMember(w, member.id));
|
|
29
|
+
// kind 別にグループ化する
|
|
30
|
+
const kindMap = new Map();
|
|
31
|
+
for (const w of memberWarnings) {
|
|
32
|
+
const existing = kindMap.get(w.kind);
|
|
33
|
+
if (existing) {
|
|
34
|
+
existing.push(w);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
kindMap.set(w.kind, [w]);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const groups = Array.from(kindMap.entries()).map(([kind, ws]) => ({
|
|
41
|
+
kind,
|
|
42
|
+
warnings: ws,
|
|
43
|
+
}));
|
|
44
|
+
return {
|
|
45
|
+
member,
|
|
46
|
+
groups,
|
|
47
|
+
totalWarnings: memberWarnings.length,
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
return { members: results, noAiMembers: false };
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* warning が特定のメンバー ID に関係するか判定する。
|
|
54
|
+
*
|
|
55
|
+
* 帰属ルール:
|
|
56
|
+
* - `mode_violation`: message に memberId が含まれる (board.ts が `AI (${created_by})` と書く)
|
|
57
|
+
* - `unregistered_member`: message に `"${memberId}"` が含まれる (board.ts が `created_by "${id}"` / `assignee "${id}"` と書く)
|
|
58
|
+
* - `invalid_task` / `duplicate_id`: タスク起票者を message から特定できないが、メンバーが 1 人なら
|
|
59
|
+
* そのメンバーに帰属させる。複数の AI メンバーがいる場合は全員に共通の問題として扱う (= 全員に紐付ける)
|
|
60
|
+
* - `invalid_config`: config 全体の問題なので全 AI メンバーに共通 (= 全員に紐付ける)
|
|
61
|
+
*/
|
|
62
|
+
function isWarningForMember(warning, memberId) {
|
|
63
|
+
switch (warning.kind) {
|
|
64
|
+
case 'mode_violation':
|
|
65
|
+
// board.ts: `AI (${frontmatter.created_by}) が ...`
|
|
66
|
+
return warning.message.includes(`(${memberId})`);
|
|
67
|
+
case 'unregistered_member':
|
|
68
|
+
// board.ts: `created_by "${id}" は config.members に未登録` / `assignee "${id}" ...`
|
|
69
|
+
return warning.message.includes(`"${memberId}"`);
|
|
70
|
+
case 'invalid_task':
|
|
71
|
+
case 'duplicate_id':
|
|
72
|
+
case 'invalid_config':
|
|
73
|
+
// task/config 全体の問題。AI メンバー全員に共通の診断事項として表示する
|
|
74
|
+
return true;
|
|
75
|
+
default:
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
package/dist/shared/schema.js
CHANGED
|
@@ -15,6 +15,11 @@ export const UI_LANGS = ['en', 'ja'];
|
|
|
15
15
|
export const DEFAULT_PORT = 5151;
|
|
16
16
|
/** overview モードの既定ポート (波 VII)。serve の 5151 と衝突しない値・1 箇所集約 */
|
|
17
17
|
export const DEFAULT_OVERVIEW_PORT = 5152;
|
|
18
|
+
/**
|
|
19
|
+
* serve のポート自動フォールバック試行回数。起点 (--port 明示時を除く) が使用中なら
|
|
20
|
+
* 起点〜起点+この数-1 まで順に空きを探す。複数プロジェクトの同時 serve 用 (例: 5151〜5170)。
|
|
21
|
+
*/
|
|
22
|
+
export const PORT_FALLBACK_ATTEMPTS = 20;
|
|
18
23
|
// 日付は文字列のまま扱い、Date 化しない (js-yaml JSON_SCHEMA エンジンと対になる規則)。
|
|
19
24
|
// zod の datetime({ offset: true }) を使い、月 13 / 時 25 のような実在しない日時を弾く
|
|
20
25
|
// (自前正規表現は桁数だけ見て実在性を見ず、経過時間表示が NaN になりうる — PM Round 1 Minor #8)。
|
|
@@ -145,6 +150,9 @@ export const configSchema = z.object({
|
|
|
145
150
|
views: z.array(z.object({ id: z.string().min(1), hidden: z.boolean().optional() })).default([]),
|
|
146
151
|
// 関連資料ディレクトリ (波 VIII)。プロジェクトルートからの相対パス。既定は空 = 機能オフ (UI 非表示)
|
|
147
152
|
docsDirs: z.array(z.string().min(1)).default([]),
|
|
153
|
+
// 資料の書込先ディレクトリ (資料エディタ MVP)。読取 (docsDirs) と分離し、既定は空 = 書込オフ。
|
|
154
|
+
// 書込 API (POST/PUT/DELETE /api/docs) はこの配下のみ許可する (root 配下相対)。
|
|
155
|
+
docsWriteDirs: z.array(z.string().min(1)).default([]),
|
|
148
156
|
// ごみ箱 (.kanban/.trash) の同 id あたり退避世代の保持件数。任意・後方互換 (未設定は DEFAULT_TRASH_KEEP)。
|
|
149
157
|
// 破壊的書込の直前に旧版を .trash へコピー退避し、この件数を超えた古い世代を刈り取る (誤上書きの安全網)
|
|
150
158
|
trashKeep: z.number().int().positive().optional(),
|
|
@@ -275,6 +283,9 @@ export const newTaskInputSchema = z
|
|
|
275
283
|
labels: z.array(z.string().min(1)).optional(),
|
|
276
284
|
// 本文 (波 VI: テンプレートからのプリフィル用)。省略時はサーバーが既定本文テンプレートを使う
|
|
277
285
|
body: z.string().optional(),
|
|
286
|
+
// 波 VIII: 関連資料 (プロジェクトルートからの相対パス配列)。任意・後方互換。
|
|
287
|
+
// 範囲 (docsDirs / docsWriteDirs 配下か) はサーバー側 assertRelatedDocsAllowed が検証する
|
|
288
|
+
related_docs: z.array(z.string().min(1)).optional(),
|
|
278
289
|
})
|
|
279
290
|
.strict();
|
|
280
291
|
/**
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* task frontmatter の 1 フィールドだけを byte 保全で書き換える純関数。
|
|
3
|
+
* setRelatedDocsInRaw (related-docs.ts) と同じ行差し替え方式を採用する。
|
|
4
|
+
*
|
|
5
|
+
* - setDependsOnInRaw: frontmatter の depends_on 配列だけを置換/追加する
|
|
6
|
+
* - setPriorityInRaw: frontmatter の priority スカラだけを置換/追加する
|
|
7
|
+
*
|
|
8
|
+
* 本文・他フィールド・改行コード・コメントは完全に保全する。
|
|
9
|
+
* frontmatter が見つからない raw はそのまま返す。
|
|
10
|
+
*/
|
|
11
|
+
// frontmatter 境界: 先頭の `---` 行 (行末空白を許容、parseTaskFile / setRelatedDocsInRaw と同じ規約)
|
|
12
|
+
const FM_OPEN = /^---[ \t]*\r?\n/;
|
|
13
|
+
// 閉じ `---` 行 (行末空白許容)
|
|
14
|
+
const FM_CLOSE = /^---[ \t]*$/;
|
|
15
|
+
// BOM は literal 禁止 (LEARN#13: invisible unicode は \uXXXX escape で書く)
|
|
16
|
+
const BOM = '\uFEFF';
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// 内部ユーティリティ
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
/**
|
|
21
|
+
* raw から BOM を剥がし、処理後に復元するラッパー。
|
|
22
|
+
* setRelatedDocsInRaw と同じ規約 (R13-1)。
|
|
23
|
+
*/
|
|
24
|
+
function withBomHandling(raw, fn) {
|
|
25
|
+
const hasBom = raw.startsWith(BOM);
|
|
26
|
+
const body = hasBom ? raw.slice(1) : raw;
|
|
27
|
+
const result = fn(body);
|
|
28
|
+
return hasBom ? BOM + result : result;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* frontmatter 内の指定 key の行 (flow 1 行 or block 形式 key+続く`- `行) を除去し、
|
|
32
|
+
* フロントマターの行配列 (開き`---`を除く、閉じ`---`を除く範囲) を返す。
|
|
33
|
+
* また閉じ `---` のインデックスと全行配列も返す。
|
|
34
|
+
*/
|
|
35
|
+
function parseFrontmatterLines(raw) {
|
|
36
|
+
if (!FM_OPEN.test(raw))
|
|
37
|
+
return null;
|
|
38
|
+
const lines = raw.split('\n');
|
|
39
|
+
let closeIdx = -1;
|
|
40
|
+
for (let i = 1; i < lines.length; i++) {
|
|
41
|
+
if (FM_CLOSE.test(lines[i].replace(/\r$/, ''))) {
|
|
42
|
+
closeIdx = i;
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (closeIdx === -1)
|
|
47
|
+
return null;
|
|
48
|
+
return { lines, closeIdx };
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* frontmatter 内の指定 key の行 (flow or block) を除去した行配列を返す。
|
|
52
|
+
* kept は開き `---` の次から closeIdx の手前までの内容 (閉じ `---` は含まない)。
|
|
53
|
+
*/
|
|
54
|
+
function removeKeyLines(lines, closeIdx, key) {
|
|
55
|
+
const keyPattern = new RegExp(`^${key}\\s*:`);
|
|
56
|
+
const kept = [];
|
|
57
|
+
let i = 1;
|
|
58
|
+
while (i < closeIdx) {
|
|
59
|
+
const line = lines[i].replace(/\r$/, '');
|
|
60
|
+
if (keyPattern.test(line)) {
|
|
61
|
+
i++;
|
|
62
|
+
// block 形式: 続く `- ` 行も取り除く (ゼロインデントも対象。setRelatedDocsInRaw R13-4 と同方式)
|
|
63
|
+
while (i < closeIdx && /^\s*-\s/.test(lines[i].replace(/\r$/, '')))
|
|
64
|
+
i++;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
kept.push(lines[i]);
|
|
68
|
+
i++;
|
|
69
|
+
}
|
|
70
|
+
return kept;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* `kept` 行配列にフィールドを挿入し、raw 全体を再組み立てして返す。
|
|
74
|
+
* 挿入位置: created: 行の直前、なければ末尾 (setRelatedDocsInRaw と同じ規約)。
|
|
75
|
+
* newLine が null の場合は挿入しない (フィールド除去のみ)。
|
|
76
|
+
*/
|
|
77
|
+
function rebuildRaw(lines, closeIdx, kept, newLine) {
|
|
78
|
+
if (newLine !== null) {
|
|
79
|
+
const createdIdx = kept.findIndex((l) => /^created\s*:/.test(l.replace(/\r$/, '')));
|
|
80
|
+
if (createdIdx !== -1) {
|
|
81
|
+
kept.splice(createdIdx, 0, newLine);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
kept.push(newLine);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const head = [lines[0], ...kept].join('\n');
|
|
88
|
+
const tail = lines.slice(closeIdx).join('\n');
|
|
89
|
+
return `${head}\n${tail}`;
|
|
90
|
+
}
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// 公開 API
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
/**
|
|
95
|
+
* raw (frontmatter + 本文) の depends_on 行を ids で差し替えた raw を返す。
|
|
96
|
+
* - ids が空: depends_on 行 (flow 1 行 or block 形式) を除去する
|
|
97
|
+
* - ids が非空: JSON.stringify 済み flow 形式 1 行
|
|
98
|
+
* (`depends_on: ["TASK-001", "TASK-002"]`) に置換/挿入する。
|
|
99
|
+
* setRelatedDocsInRaw と同じ表現スタイルに統一する (仕様「配列表現は existing に合わせる」)。
|
|
100
|
+
* - frontmatter が無い raw はそのまま返す
|
|
101
|
+
*/
|
|
102
|
+
export function setDependsOnInRaw(raw, ids) {
|
|
103
|
+
return withBomHandling(raw, (body) => setDependsOnInner(body, ids));
|
|
104
|
+
}
|
|
105
|
+
function setDependsOnInner(raw, ids) {
|
|
106
|
+
const parsed = parseFrontmatterLines(raw);
|
|
107
|
+
if (!parsed)
|
|
108
|
+
return raw;
|
|
109
|
+
const { lines, closeIdx } = parsed;
|
|
110
|
+
const kept = removeKeyLines(lines, closeIdx, 'depends_on');
|
|
111
|
+
// setRelatedDocsInRaw と同じ JSON.stringify flow 形式 (YAML double-quoted として常に valid)
|
|
112
|
+
const newLine = ids.length > 0 ? `depends_on: [${ids.map((id) => JSON.stringify(id)).join(', ')}]` : null;
|
|
113
|
+
return rebuildRaw(lines, closeIdx, kept, newLine);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* raw (frontmatter + 本文) の priority 行を priority で差し替えた raw を返す。
|
|
117
|
+
* - フィールドが無ければ created: 行の直前 (無ければ閉じ `---` の直前) に追加する
|
|
118
|
+
* - フィールドが既にあれば値だけ置換する
|
|
119
|
+
* - frontmatter が無い raw はそのまま返す
|
|
120
|
+
*/
|
|
121
|
+
export function setPriorityInRaw(raw, priority) {
|
|
122
|
+
return withBomHandling(raw, (body) => setPriorityInner(body, priority));
|
|
123
|
+
}
|
|
124
|
+
function setPriorityInner(raw, priority) {
|
|
125
|
+
const parsed = parseFrontmatterLines(raw);
|
|
126
|
+
if (!parsed)
|
|
127
|
+
return raw;
|
|
128
|
+
const { lines, closeIdx } = parsed;
|
|
129
|
+
const kept = removeKeyLines(lines, closeIdx, 'priority');
|
|
130
|
+
const newLine = `priority: ${priority}`;
|
|
131
|
+
return rebuildRaw(lines, closeIdx, kept, newLine);
|
|
132
|
+
}
|