@takagaki/cortex-decisions-viewer 0.4.54 → 0.4.56
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/dist/build.js +135 -43
- package/dist/render.js +155 -45
- package/package.json +1 -1
package/dist/build.js
CHANGED
|
@@ -38,6 +38,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
39
|
exports.readFleetSources = readFleetSources;
|
|
40
40
|
exports.build = build;
|
|
41
|
+
exports.createRefResolver = createRefResolver;
|
|
41
42
|
const node_fs_1 = require("node:fs");
|
|
42
43
|
const path = __importStar(require("node:path"));
|
|
43
44
|
const gray_matter_1 = __importDefault(require("gray-matter"));
|
|
@@ -351,10 +352,57 @@ async function walkMd(dirAbs) {
|
|
|
351
352
|
* - 課題キー(例: PJ_CORTEX-13)→ 同期mdに記載された Backlog Issue Link(実Backlog URL)
|
|
352
353
|
* - minute:{定例名}:{YYYYMMDD} → 議事録ファイルのGitHub blobリンク
|
|
353
354
|
*/
|
|
355
|
+
/**
|
|
356
|
+
* マーカーファイルの在り処からディレクトリ名を求める(`cortex-engine/scripts/fleet-status.mjs`
|
|
357
|
+
* の `findDirByMarker` と同じ考え方)。
|
|
358
|
+
*
|
|
359
|
+
* **ディレクトリ名をハードコードしない。** `customize-tooling` で改名している案件が実在する
|
|
360
|
+
* (ある案件は `課題管理/`→`Backlog/`・`会議/`→`MTG/`・`デザイン/`→`Figma/`)。
|
|
361
|
+
* 決め打ちだと、その案件では課題キーもFigmaも**永久に解決できない**。
|
|
362
|
+
*/
|
|
363
|
+
async function findDirByMarker(repoRoot, marker, fallback) {
|
|
364
|
+
try {
|
|
365
|
+
for (const d of await node_fs_1.promises.readdir(repoRoot, { withFileTypes: true })) {
|
|
366
|
+
if (!d.isDirectory() || d.name === "node_modules" || d.name.startsWith("."))
|
|
367
|
+
continue;
|
|
368
|
+
try {
|
|
369
|
+
await node_fs_1.promises.stat(path.join(repoRoot, d.name, marker));
|
|
370
|
+
return d.name;
|
|
371
|
+
}
|
|
372
|
+
catch {
|
|
373
|
+
// 直下に無ければ1階層下も見る(backlog-settings.json は issues/ の下に置かれる)
|
|
374
|
+
}
|
|
375
|
+
try {
|
|
376
|
+
for (const sub of await node_fs_1.promises.readdir(path.join(repoRoot, d.name), { withFileTypes: true })) {
|
|
377
|
+
if (!sub.isDirectory())
|
|
378
|
+
continue;
|
|
379
|
+
try {
|
|
380
|
+
await node_fs_1.promises.stat(path.join(repoRoot, d.name, sub.name, marker));
|
|
381
|
+
return d.name;
|
|
382
|
+
}
|
|
383
|
+
catch {
|
|
384
|
+
// 次へ
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
catch {
|
|
389
|
+
// 読めないディレクトリは飛ばす
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
catch {
|
|
394
|
+
// repoRoot が読めなければ既定名で進む
|
|
395
|
+
}
|
|
396
|
+
return fallback;
|
|
397
|
+
}
|
|
354
398
|
async function buildTargetUrlMap(repoRoot, repoBaseUrl, branch) {
|
|
355
399
|
const map = new Map();
|
|
356
|
-
|
|
357
|
-
|
|
400
|
+
const issuesDir = await findDirByMarker(repoRoot, "backlog-settings.json", "課題管理");
|
|
401
|
+
const designDir = await findDirByMarker(repoRoot, "figma.json", "デザイン");
|
|
402
|
+
const meetingDir = await findDirByMarker(repoRoot, "ingest-config.json", "会議");
|
|
403
|
+
const materialsDir = await findDirByMarker(repoRoot, "materials-config.json", "共有資料");
|
|
404
|
+
// 課題キー → Backlog URL({課題管理}/issues/ の同期mdの基本情報から抽出)
|
|
405
|
+
for (const file of await walkMd(path.join(repoRoot, issuesDir, "issues"))) {
|
|
358
406
|
try {
|
|
359
407
|
const head = (await node_fs_1.promises.readFile(file, "utf8")).slice(0, 4000);
|
|
360
408
|
const keyMatch = head.match(/^- 課題キー: (\S+)$/m);
|
|
@@ -367,7 +415,7 @@ async function buildTargetUrlMap(repoRoot, repoBaseUrl, branch) {
|
|
|
367
415
|
}
|
|
368
416
|
}
|
|
369
417
|
// material:{slug} → 共有資料の正本(Drive等のsource URL)。無ければ変換済みmdのGitHubリンク
|
|
370
|
-
for (const file of await walkMd(path.join(repoRoot,
|
|
418
|
+
for (const file of await walkMd(path.join(repoRoot, materialsDir))) {
|
|
371
419
|
if (path.basename(file).toLowerCase() === "link.md")
|
|
372
420
|
continue;
|
|
373
421
|
try {
|
|
@@ -397,22 +445,32 @@ async function buildTargetUrlMap(repoRoot, repoBaseUrl, branch) {
|
|
|
397
445
|
// 読めないファイルはスキップ
|
|
398
446
|
}
|
|
399
447
|
}
|
|
400
|
-
// design:{fileKey}:{nodeId} → Figma
|
|
401
|
-
|
|
448
|
+
// design:{fileKey}:{nodeId} → Figmaディープリンク
|
|
449
|
+
//
|
|
450
|
+
// **frontmatter からは取れない。** インベントリは frontmatter を持たない
|
|
451
|
+
// (frontmatter を持つのは Gold層だけ=オントロジー規約)。参照IDもFigmaのURLも**本文**にある:
|
|
452
|
+
// - 参照ID: `design:xxx:103:1836`
|
|
453
|
+
// - [Figmaで開く](https://www.figma.com/design/...)
|
|
454
|
+
// 以前は `data.type === "design"` を見ていたので、この解決は**一度も効いていなかった**。
|
|
455
|
+
for (const file of await walkMd(path.join(repoRoot, designDir, "inventory"))) {
|
|
402
456
|
try {
|
|
403
|
-
const
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
457
|
+
const body = await node_fs_1.promises.readFile(file, "utf8");
|
|
458
|
+
const id = body.match(/^- 参照ID: `(design:[^`]+)`\s*$/m);
|
|
459
|
+
const url = body.match(/^- \[Figmaで開く\]\((https?:[^)]+)\)\s*$/m);
|
|
460
|
+
if (id && url)
|
|
461
|
+
map.set(id[1], url[1]);
|
|
407
462
|
}
|
|
408
463
|
catch {
|
|
409
464
|
// 読めないファイルはスキップ
|
|
410
465
|
}
|
|
411
466
|
}
|
|
412
467
|
// minute:{定例名}:{YYYYMMDD} → 議事録ファイルのGitHubリンク
|
|
413
|
-
// パス規約:
|
|
468
|
+
// パス規約: {会議}/{フェーズ}/{定例名}/{YYYYMMDD}/*minutes*.md
|
|
469
|
+
//
|
|
470
|
+
// **以前は存在しない `ミーティング/` を見ていた**(艦隊は全案件 `会議/`。改名案件は `MTG/`)。
|
|
471
|
+
// そのため minute: の解決は一度も効いていなかった。
|
|
414
472
|
if (repoBaseUrl) {
|
|
415
|
-
for (const file of await walkMd(path.join(repoRoot,
|
|
473
|
+
for (const file of await walkMd(path.join(repoRoot, meetingDir))) {
|
|
416
474
|
const name = path.basename(file).toLowerCase();
|
|
417
475
|
if (!name.includes("minutes"))
|
|
418
476
|
continue;
|
|
@@ -567,7 +625,7 @@ async function scanContent(repoRoot, goldDirName, repoBaseUrl, branch) {
|
|
|
567
625
|
* 参照先がリポジトリ内のレコードでないIDは、層を推定したファントムノードとして表示する
|
|
568
626
|
* (minute:/material:→Silver、課題キー・外部URL→Bronze、Goldの未解決ID→phantom)。
|
|
569
627
|
*/
|
|
570
|
-
function buildGraph(decisions, home, sections, content, targetUrls, repoBaseUrl, branch) {
|
|
628
|
+
function buildGraph(decisions, home, sections, content, targetUrls, repoBaseUrl, branch, refs) {
|
|
571
629
|
const nodes = new Map();
|
|
572
630
|
const edges = [];
|
|
573
631
|
const addNode = (node) => {
|
|
@@ -627,7 +685,12 @@ function buildGraph(decisions, home, sections, content, targetUrls, repoBaseUrl,
|
|
|
627
685
|
else if (target.includes("/") || target.toLowerCase().endsWith(".md")) {
|
|
628
686
|
group = "silver"; // リポジトリ内ファイルへのパス参照 → GitHubのコピーへ飛べるようにする
|
|
629
687
|
label = target.split("/").pop() ?? target;
|
|
630
|
-
|
|
688
|
+
// **実在するものだけリンクにする。** 詳細画面(resolveReference)と同じ判定を使う。
|
|
689
|
+
// ここだけ無条件に blob URL を作っていたため、同じ参照が「詳細ではテキスト・
|
|
690
|
+
// グラフでは404リンク」に分かれていた(グラフのノードは window.open される)。
|
|
691
|
+
const blobUrl = repoBaseUrl && refs.exists(target)
|
|
692
|
+
? `${repoBaseUrl}/blob/${branch}/${encodePath(target)}`
|
|
693
|
+
: undefined;
|
|
631
694
|
return addNode({ id: target, label, group, url: blobUrl });
|
|
632
695
|
}
|
|
633
696
|
return addNode({ id: target, label, group });
|
|
@@ -948,15 +1011,23 @@ async function build(opts) {
|
|
|
948
1011
|
// (Decisionsディレクトリで実行すると `-- Cortex/` に一致せず常に空になる)。
|
|
949
1012
|
// 浅いclone・git無しの環境では空集合が返り、「最新」ラベルが出ないだけで他の表示は壊れない。
|
|
950
1013
|
LATEST_PATHS = (0, repo_1.latestGeneratedPaths)(process.cwd());
|
|
1014
|
+
// Cortex構成なら、ホームと「records/ を持つディレクトリ」をすべて読み込む(旧構成では空)
|
|
1015
|
+
// ディレクトリ名はハードコードしない: 案件が新しいレコード種別のディレクトリを足せば、そのままタブとして表示される
|
|
1016
|
+
const knowledgeRoot = resolveKnowledgeRoot(dirAbs);
|
|
1017
|
+
// **参照の解決器は decisions のパースより前に作る。** parseDecision が references を
|
|
1018
|
+
// 解決するので、あとから作ると空の表・空の実在判定が渡り、**実在するパスまで
|
|
1019
|
+
// 「リンクにしない」に倒れて全滅する**(以前は順序が逆だった)。
|
|
1020
|
+
const repoRoot = knowledgeRoot ? path.dirname(knowledgeRoot) : path.dirname(dirAbs);
|
|
1021
|
+
const targetUrls = knowledgeRoot
|
|
1022
|
+
? await buildTargetUrlMap(repoRoot, repo.baseUrl, repo.branch)
|
|
1023
|
+
: new Map();
|
|
1024
|
+
const refs = createRefResolver({ repoRoot, targetUrls, repoBaseUrl: repo.baseUrl, branch: repo.branch });
|
|
951
1025
|
const decisions = [];
|
|
952
1026
|
for (const fileName of mdFiles) {
|
|
953
1027
|
const raw = await node_fs_1.promises.readFile(path.join(dirAbs, fileName), "utf8");
|
|
954
|
-
const rec = await parseDecision(raw, fileName, repoRelDir, repo.baseUrl, repo.branch);
|
|
1028
|
+
const rec = await parseDecision(raw, fileName, repoRelDir, repo.baseUrl, repo.branch, refs);
|
|
955
1029
|
decisions.push(rec);
|
|
956
1030
|
}
|
|
957
|
-
// Cortex構成なら、ホームと「records/ を持つディレクトリ」をすべて読み込む(旧構成では空)
|
|
958
|
-
// ディレクトリ名はハードコードしない: 案件が新しいレコード種別のディレクトリを足せば、そのままタブとして表示される
|
|
959
|
-
const knowledgeRoot = resolveKnowledgeRoot(dirAbs);
|
|
960
1031
|
let home = null;
|
|
961
1032
|
const sections = [];
|
|
962
1033
|
if (knowledgeRoot) {
|
|
@@ -989,12 +1060,8 @@ async function build(opts) {
|
|
|
989
1060
|
const categories = uniqueSorted(decisions.map((d) => d.category).filter(Boolean));
|
|
990
1061
|
// 期間フィルタ用の月(YYYY-MM)を日付から導出し、降順(新しい順)にする
|
|
991
1062
|
const months = uniqueSorted(decisions.map((d) => d.date.slice(0, 7)).filter((m) => /^\d{4}-\d{2}$/.test(m))).reverse();
|
|
992
|
-
const repoRoot = knowledgeRoot ? path.dirname(knowledgeRoot) : path.dirname(dirAbs);
|
|
993
|
-
const targetUrls = knowledgeRoot
|
|
994
|
-
? await buildTargetUrlMap(repoRoot, repo.baseUrl, repo.branch)
|
|
995
|
-
: new Map();
|
|
996
1063
|
const contentEntries = await scanContent(repoRoot, knowledgeRoot ? path.basename(knowledgeRoot) : null, repo.baseUrl, repo.branch);
|
|
997
|
-
const graph = buildGraph(decisions, home, sections, contentEntries, targetUrls, repo.baseUrl, repo.branch);
|
|
1064
|
+
const graph = buildGraph(decisions, home, sections, contentEntries, targetUrls, repo.baseUrl, repo.branch, refs);
|
|
998
1065
|
computeLayout(graph); // 各ノードに x/y を書き込む(クライアントはこれを初期位置に使いウォームアップ省略)
|
|
999
1066
|
// データソースと自動化(fleet-status.json 由来)。無ければ undefined でセクション非表示。
|
|
1000
1067
|
const fleet = await readFleetSources(repoRoot);
|
|
@@ -1055,7 +1122,7 @@ async function build(opts) {
|
|
|
1055
1122
|
const sectionCount = sections.reduce((n, s) => n + s.records.length, 0);
|
|
1056
1123
|
return { count: decisions.length + (home ? 1 : 0) + sectionCount, outDir: outAbs };
|
|
1057
1124
|
}
|
|
1058
|
-
async function parseDecision(raw, fileName, repoRelDir, repoBaseUrl, branch) {
|
|
1125
|
+
async function parseDecision(raw, fileName, repoRelDir, repoBaseUrl, branch, refs) {
|
|
1059
1126
|
const { data, content } = (0, gray_matter_1.default)(raw);
|
|
1060
1127
|
const id = String(data.id ?? fileName.replace(/\.md$/i, ""));
|
|
1061
1128
|
const title = String(data.title ?? stripIdPrefix(fileName));
|
|
@@ -1065,7 +1132,7 @@ async function parseDecision(raw, fileName, repoRelDir, repoBaseUrl, branch) {
|
|
|
1065
1132
|
const summary = data.description != null ? String(data.description) : data.summary != null ? String(data.summary) : ""; // description優先(旧summaryは移行期互換)
|
|
1066
1133
|
const deciders = toStringArray(data.deciders);
|
|
1067
1134
|
const referencesRaw = toStringArray(data.references);
|
|
1068
|
-
const references = referencesRaw.map((r) =>
|
|
1135
|
+
const references = referencesRaw.map((r) => refs.resolve(r));
|
|
1069
1136
|
const relations = toRelations(data.relations);
|
|
1070
1137
|
// 本文先頭の H1 はタイトルと重複するため除去(詳細画面ではタイトルを別途表示する)
|
|
1071
1138
|
const bodyHtml = externalLinksToNewTab((await marked_1.marked.parse(content, { async: true })).replace(/^\s*<h1[^>]*>[\s\S]*?<\/h1>\s*/, ""));
|
|
@@ -1095,25 +1162,50 @@ async function parseDecision(raw, fileName, repoRelDir, repoBaseUrl, branch) {
|
|
|
1095
1162
|
fmErrors: validateFm(data),
|
|
1096
1163
|
};
|
|
1097
1164
|
}
|
|
1098
|
-
function
|
|
1099
|
-
const
|
|
1100
|
-
//
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1165
|
+
function createRefResolver(opts) {
|
|
1166
|
+
const { repoRoot, targetUrls, repoBaseUrl, branch } = opts;
|
|
1167
|
+
// **走査結果を流用しない。** scanContent は `.md` しか歩かず SCAN_SKIP_DIRS を除外するので、
|
|
1168
|
+
// 実在する `.txt`(Geminiメモ)や `tmp/` 配下のファイルを「実在しない」と誤判定して
|
|
1169
|
+
// **いま動いているリンクを消す**。参照は1リポジトリあたり数百件なので stat で十分。
|
|
1170
|
+
const seen = new Map();
|
|
1171
|
+
const exists = (relPath) => {
|
|
1172
|
+
const hit = seen.get(relPath);
|
|
1173
|
+
if (hit !== undefined)
|
|
1174
|
+
return hit;
|
|
1175
|
+
let ok = false;
|
|
1176
|
+
try {
|
|
1177
|
+
// パス要素に混ざる制御文字・絶対パス・親ディレクトリ参照は見に行かない
|
|
1178
|
+
if (relPath && !path.isAbsolute(relPath) && !relPath.split("/").includes("..")) {
|
|
1179
|
+
(0, node_fs_1.statSync)(path.join(repoRoot, relPath));
|
|
1180
|
+
ok = true;
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
catch {
|
|
1184
|
+
ok = false;
|
|
1185
|
+
}
|
|
1186
|
+
seen.set(relPath, ok);
|
|
1187
|
+
return ok;
|
|
1188
|
+
};
|
|
1189
|
+
const resolve = (ref) => {
|
|
1190
|
+
const trimmed = String(ref ?? "").trim();
|
|
1191
|
+
if (!trimmed)
|
|
1192
|
+
return { label: trimmed, url: null };
|
|
1193
|
+
const mdLink = trimmed.match(/^\[([^\]]+)\]\(([^)]+)\)$/);
|
|
1194
|
+
if (mdLink)
|
|
1195
|
+
return { label: mdLink[1], url: mdLink[2] };
|
|
1196
|
+
if (/^https?:\/\//.test(trimmed))
|
|
1197
|
+
return { label: trimmed, url: trimmed };
|
|
1198
|
+
// 正本のURL(課題キー・material:・design:・minute:)。**ここが「ツールへ飛ぶ」の本体**
|
|
1199
|
+
const known = targetUrls.get(trimmed);
|
|
1200
|
+
if (known)
|
|
1201
|
+
return { label: trimmed, url: known };
|
|
1202
|
+
// 実在するファイルだけ blob URL にする
|
|
1203
|
+
if (repoBaseUrl && exists(trimmed)) {
|
|
1204
|
+
return { label: trimmed, url: `${repoBaseUrl}/blob/${branch}/${encodePath(trimmed)}` };
|
|
1205
|
+
}
|
|
1206
|
+
return { label: trimmed, url: null };
|
|
1207
|
+
};
|
|
1208
|
+
return { resolve, exists };
|
|
1117
1209
|
}
|
|
1118
1210
|
/** dirをリポジトリルート相対(posix)に。cwd配下でなければ basename を返す。 */
|
|
1119
1211
|
function toRepoRelative(dir) {
|
package/dist/render.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.renderSite = renderSite;
|
|
4
|
+
exports.renderConnectionMap = renderConnectionMap;
|
|
4
5
|
/** SiteDataを単一の自己完結HTML(埋め込みデータ+CSS+JS)にレンダリングする */
|
|
5
6
|
function renderSite(site) {
|
|
6
7
|
// </script> によるブレイクアウトを防ぐため < をエスケープして埋め込む
|
|
@@ -140,36 +141,94 @@ function renderConnectionMap(site) {
|
|
|
140
141
|
// マップが⚠️なのに直下のグループが「接続OK」という矛盾が同じ画面に出る。
|
|
141
142
|
? liveExts.some((s) => sourceNeedsAttention(s))
|
|
142
143
|
: pipes.some((p) => c.pipe != null && c.pipe.test(p.id) && p.applicable !== false && p.lastConclusion === "failure"));
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
144
|
+
// **宣言されているだけで、実際には何も繋がっていない**状態を分けて表す。
|
|
145
|
+
//
|
|
146
|
+
// 会議に Teams、チャットに Teams を宣言した案件で、どちらも「接続済み」の見た目になっていた。
|
|
147
|
+
// notetaker は Teams の文字起こしを取り込めず(Google Meet 前提)、Teams の MCP も配っていないので、
|
|
148
|
+
// **実体は何も繋がっていない**。それを繋がって見せると、設定が足りないことに誰も気づけない。
|
|
149
|
+
//
|
|
150
|
+
// - 会議: 取り込みが有効(ingestState === "on")でなければ「自動連携なし」
|
|
151
|
+
// - チャット/開発: ライブ参照の実体(登録されたチャンネル・リポジトリ)が無ければ同じ
|
|
152
|
+
//
|
|
153
|
+
// **能力ごとに場当たりで判定しない。** 最初は会議とライブ参照だけを見ていたが、
|
|
154
|
+
// 課題管理が「宣言があるだけ」で接続済み(青)に見えていた——同期が一度も走っておらず、
|
|
155
|
+
// APIキーすら入っていない案件で。指摘されるまで気づけなかった。
|
|
156
|
+
//
|
|
157
|
+
// 「実際に繋がっている」の証拠は共通で次のどれか:
|
|
158
|
+
// - 一度でも同期が走った(lastSync)
|
|
159
|
+
// - 正本のURLが導出できる(url。設定ファイルが実在して初めて出る)
|
|
160
|
+
// - 会議は取り込みが有効(ingestState === "on")
|
|
161
|
+
// - ライブ参照は読む対象が1件以上ある
|
|
162
|
+
const idle = !off &&
|
|
163
|
+
(isLive
|
|
164
|
+
? liveExts.length === 0
|
|
165
|
+
: c.cap === "会議"
|
|
166
|
+
? internal?.ingestState !== "on"
|
|
167
|
+
: !(internal?.lastSync || internal?.url));
|
|
168
|
+
const state = off ? "cm-off" : warn ? "cm-warn" : idle ? "cm-idle" : "cm-ok";
|
|
169
|
+
// **ラベルは能力名(会議・課題管理…)。ツール名は下に小さく置く。**
|
|
170
|
+
// ツールは「その能力をいま何で実現しているか」という差し替え可能なアダプタで、
|
|
171
|
+
// 主語ではない。ツール名を主にすると、Teams のように複数の能力に使われるものが
|
|
172
|
+
// 同じ名前で並び(会議とチャットに Teams が2つ)、何が繋がっているのか読めなくなる。
|
|
173
|
+
// "none"(未使用の宣言)はツール名ではないので出さない。そのまま表示すると
|
|
174
|
+
// 「ツール: none」という読めない行になる(実際にそう出ていた)
|
|
175
|
+
const rawTool = toolVal && toolVal !== "none"
|
|
176
|
+
? toolVal
|
|
177
|
+
: internal && internal.tool && internal.tool !== "none"
|
|
178
|
+
? internal.tool
|
|
148
179
|
: isLive && liveExts.length
|
|
149
|
-
?
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
180
|
+
? c.cap === "チャット"
|
|
181
|
+
? "slack"
|
|
182
|
+
: "github"
|
|
183
|
+
: "";
|
|
184
|
+
const toolName = rawTool ? CM_TOOL_LABEL[rawTool] ?? rawTool : "";
|
|
185
|
+
const label = `${c.icon} ${c.cap}`;
|
|
186
|
+
// 2行目=ツール名(小さく)/3行目=状態(未使用・自動連携なし・ライブ参照・最終同期)
|
|
187
|
+
const toolLine = toolName
|
|
188
|
+
? `<text class="cm-tool" x="${cx}" y="53" text-anchor="middle">${escapeHtml(toolName)}</text>`
|
|
189
|
+
: "";
|
|
190
|
+
let statusText = "";
|
|
191
|
+
let statusTs = "";
|
|
154
192
|
if (off) {
|
|
155
|
-
|
|
193
|
+
statusText = "未使用";
|
|
194
|
+
}
|
|
195
|
+
else if (idle) {
|
|
196
|
+
// **「未接続」と言い切る。** 「自動連携なし」だと運用でそう決めたようにも読めるが、
|
|
197
|
+
// 実際は「宣言はあるが、まだ一度も繋がっていない」——やることが残っている状態
|
|
198
|
+
statusText = "未接続";
|
|
156
199
|
}
|
|
157
200
|
else if (isLive) {
|
|
158
|
-
|
|
201
|
+
statusText = "ライブ参照";
|
|
159
202
|
}
|
|
160
203
|
else if (internal?.lastSync) {
|
|
161
|
-
|
|
204
|
+
statusTs = internal.lastSync;
|
|
162
205
|
}
|
|
163
|
-
|
|
206
|
+
// 時刻はクライアントでローカルタイム整形するため data-ts で渡す
|
|
207
|
+
const sub = statusText || statusTs
|
|
208
|
+
? `<text class="cm-sync" x="${cx}" y="67" text-anchor="middle"` +
|
|
209
|
+
(statusTs ? ` data-ts="${escapeHtml(statusTs)}"` : "") +
|
|
210
|
+
`>${escapeHtml(statusText)}</text>`
|
|
211
|
+
: "";
|
|
212
|
+
const badge = warn ? `<text class="cm-badge" x="${cx + 62}" y="24" text-anchor="middle">⚠️</text>` : "";
|
|
164
213
|
// クリックでページ内の該当詳細カード(アンカー)へスクロールする
|
|
165
214
|
const anchor = `ds-${c.cap}`;
|
|
166
215
|
nodes.push(`<g class="cm-node ${state}" data-target="${anchor}" tabindex="0" role="link" aria-label="${escapeHtml(c.cap)}の詳細へ">` +
|
|
167
|
-
`<rect x="${cx - 74}" y="
|
|
168
|
-
`<text class="cm-label" x="${cx}" y="
|
|
216
|
+
`<rect x="${cx - 74}" y="12" width="148" height="64" rx="10"></rect>` +
|
|
217
|
+
`<text class="cm-label" x="${cx}" y="35" text-anchor="middle">${escapeHtml(label)}</text>` +
|
|
218
|
+
toolLine +
|
|
169
219
|
sub +
|
|
170
220
|
badge +
|
|
171
221
|
`</g>`);
|
|
172
|
-
|
|
222
|
+
// **線は「実際に繋がっていることが証明できたもの」だけに引く。**
|
|
223
|
+
// 未使用(cm-off)・宣言だけで実体が無いもの(cm-idle)からは線を出さない。
|
|
224
|
+
// 薄い線でも引いてしまうと、データが流れていない経路が流れているように読める——
|
|
225
|
+
// 実際、同期が一度も走っていない課題管理が、リポジトリと線で結ばれて見えていた。
|
|
226
|
+
//
|
|
227
|
+
// cm-warn は引く。**壊れているのは「繋がったうえで失敗している」状態**で、
|
|
228
|
+
// 経路自体は存在する(線を消すと、設定が無いのと区別が付かなくなる)。
|
|
229
|
+
if (state === "cm-ok" || state === "cm-warn") {
|
|
230
|
+
links.push(`<path class="cm-link ${state}" d="M ${cx} 78 C ${cx} 152, ${REPO_X} 162, ${REPO_X} 238"></path>`);
|
|
231
|
+
}
|
|
173
232
|
});
|
|
174
233
|
const repoTitle = site.title.length > 24 ? site.title.slice(0, 23) + "…" : site.title;
|
|
175
234
|
const repo = `<g class="cm-repo">` +
|
|
@@ -719,17 +778,30 @@ a.ds-name:hover { text-decoration: underline; }
|
|
|
719
778
|
.cm-node.cm-ok rect { stroke: var(--accent); }
|
|
720
779
|
.cm-node.cm-warn rect { fill: #fffbeb; stroke: #fcd34d; }
|
|
721
780
|
.cm-node.cm-off rect { fill: #f8f9fa; stroke: #d1d5db; stroke-dasharray: 4 3; }
|
|
781
|
+
/* 宣言はあるが実体が繋がっていない。未使用(グレー)とも接続済み(青)とも違うことを見せる */
|
|
782
|
+
.cm-node.cm-idle rect { fill: var(--surface); stroke: #cbd5e1; stroke-dasharray: 4 3; }
|
|
783
|
+
.cm-node.cm-idle .cm-sync { fill: #b45309; }
|
|
722
784
|
.cm-label { font-size: 13.5px; font-weight: 600; fill: var(--text); }
|
|
723
785
|
.cm-node.cm-warn .cm-label { fill: #92400e; }
|
|
724
786
|
.cm-node.cm-off .cm-label { fill: #9ca3af; }
|
|
725
787
|
.cm-node.cm-off text { filter: grayscale(1); opacity: .8; }
|
|
788
|
+
.cm-tool { font-size: 11px; fill: var(--muted); }
|
|
789
|
+
.cm-node.cm-off .cm-tool { fill: #9ca3af; }
|
|
726
790
|
.cm-sync { font-size: 10.5px; fill: var(--muted); font-variant-numeric: tabular-nums; }
|
|
727
791
|
.cm-node.cm-off .cm-sync { fill: #9ca3af; }
|
|
728
792
|
.cm-badge { font-size: 13px; }
|
|
793
|
+
/* 追加した直後にその場で見せる行。正式な一覧(スナップショット由来)に載るまでの繋ぎ */
|
|
794
|
+
.ds-pending { margin-top: .5rem; display: flex; flex-direction: column; gap: .25rem; }
|
|
795
|
+
.ds-pending-item { display: flex; align-items: center; gap: .5rem; font-size: .85rem;
|
|
796
|
+
padding: .35rem .6rem; border: 1px dashed var(--accent); border-radius: 6px; background: var(--accent-weak); }
|
|
797
|
+
.ds-pending-mark { color: var(--accent); font-weight: 700; }
|
|
798
|
+
.ds-pending-name { font-weight: 600; word-break: break-all; }
|
|
799
|
+
.ds-pending-note { color: var(--muted); font-size: .78rem; margin-left: auto; white-space: nowrap; }
|
|
729
800
|
.cm-link { fill: none; stroke-width: 1.6; stroke-dasharray: 6 6; stroke: #94a3b8; }
|
|
730
801
|
.cm-link.cm-ok { stroke: var(--accent); animation: cm-flow 1.1s linear infinite; }
|
|
731
802
|
.cm-link.cm-warn { stroke: #d97706; }
|
|
732
803
|
.cm-link.cm-off { stroke: #d1d5db; opacity: .6; }
|
|
804
|
+
.cm-link.cm-idle { stroke: #cbd5e1; opacity: .7; }
|
|
733
805
|
@keyframes cm-flow { to { stroke-dashoffset: -24; } }
|
|
734
806
|
@media (prefers-reduced-motion: reduce) { .cm-link.cm-ok { animation: none; } }
|
|
735
807
|
.cm-repo rect { fill: var(--accent-weak); stroke: var(--accent); stroke-width: 1.5; }
|
|
@@ -1846,32 +1918,41 @@ const CLIENT_JS = `
|
|
|
1846
1918
|
var tools = SITE.tools || {};
|
|
1847
1919
|
var usable = function (cap, tool) { var v = tools[cap]; return !v || v === "none" || v === tool; };
|
|
1848
1920
|
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1921
|
+
// **Slack は tools 宣言で隠さない。** tools.チャット は能力ごとに1値しか持てないので、
|
|
1922
|
+
// 「会議はTeams・連絡はSlack」という普通の構成を表せない。teams を宣言した案件では
|
|
1923
|
+
// フォームごと消え、**Slackを渡されているのに追加できない**状態になっていた。
|
|
1924
|
+
//
|
|
1925
|
+
// 読む側(resolve-external-sources.mjs)も Slack の tools ゲートを外してある。
|
|
1926
|
+
// Gold昇格の本来のゲートは**チャンネル単位の gold: true**(追加直後は未宣言=読まれない。
|
|
1927
|
+
// 上げたければ追加後にトグルを押す)。そこは変えていないので、宣言なしで顧客可視のGoldへ
|
|
1928
|
+
// 流れることはない。
|
|
1929
|
+
//
|
|
1930
|
+
// 注: ここは埋め込みJSのテンプレートリテラル内。**コメントでもバッククォートを使わない**
|
|
1931
|
+
//(リテラルが閉じて構文エラーになる。実際にそれで壊した)。
|
|
1932
|
+
box.appendChild(addForm({
|
|
1933
|
+
title: "+ Slackチャンネルを追加",
|
|
1934
|
+
placeholder: "https://…slack.com/archives/C0XXXXXXX",
|
|
1935
|
+
// **名前は任意。** URLにはIDしか入っていないので、サーバがSlackから引く。
|
|
1936
|
+
// 人に入力させると打ち間違いがそのまま登録されるうえ、手間も増える。
|
|
1937
|
+
// 別名を付けたいときだけ入れてもらう。
|
|
1938
|
+
namePlaceholder: "表示名(省略するとSlackから取得します)",
|
|
1939
|
+
requiredNote: "チャンネルのURLを入力してください。",
|
|
1940
|
+
hint: "チャンネル名を右クリック →「リンクをコピー」で出るURLを貼ってください。追加しただけでは Gold 昇格には載りません(載せるには各チャンネルの「Gold昇格を始める」を押します)。cortex の Bot が入っていないチャンネルは名前を取得できません(「名前未取得」で登録され、Bot招待後にGold昇格をONにすると実名に変わります)。",
|
|
1941
|
+
// **「Goldに載る」と書かない。** 追加だけでは載らないので嘘になる。
|
|
1942
|
+
// 確認する理由は「中央Botで読みに行く」「他案件の取り違え」。
|
|
1943
|
+
confirmLines: function (v) {
|
|
1944
|
+
return [
|
|
1945
|
+
"このチャンネルを、Cortex が読みに行く対象に追加します。",
|
|
1946
|
+
"",
|
|
1947
|
+
"貼ったURLがこの案件のものか、もう一度確認してください。",
|
|
1948
|
+
"",
|
|
1949
|
+
v,
|
|
1950
|
+
"",
|
|
1951
|
+
"追加しますか?",
|
|
1952
|
+
];
|
|
1953
|
+
},
|
|
1954
|
+
build: function (url, name) { return { op: "add-channel", url: url, name: name }; },
|
|
1955
|
+
}));
|
|
1875
1956
|
|
|
1876
1957
|
if (usable("開発", "github")) {
|
|
1877
1958
|
box.appendChild(addForm({
|
|
@@ -1938,8 +2019,20 @@ const CLIENT_JS = `
|
|
|
1938
2019
|
msg.appendChild(document.createTextNode(" "));
|
|
1939
2020
|
msg.appendChild(el("a", { href: j.runsUrl, target: "_blank", rel: "noopener", text: "実行状況を見る →" }));
|
|
1940
2021
|
}
|
|
2022
|
+
// **画面への反映も起きていることを伝える。** 一覧はスナップショットから描かれるので、
|
|
2023
|
+
// 「保存した」だけでは「一覧が変わらない=失敗した」と読まれる。
|
|
2024
|
+
// 数分かかること・再読み込みで正式な一覧に載ることまで書く(黙っていると待てない)
|
|
2025
|
+
// 反映にかかる時間はサーバの文言(reflectionNote)が言うので、ここでは繰り返さない。
|
|
2026
|
+
// 二重に書くと同じ一文に同じ話が並ぶ
|
|
2027
|
+
if (j.refreshUrl) {
|
|
2028
|
+
msg.appendChild(document.createTextNode(" "));
|
|
2029
|
+
msg.appendChild(el("a", { href: j.refreshUrl, target: "_blank", rel: "noopener", text: "反映の状況 →" }));
|
|
2030
|
+
}
|
|
1941
2031
|
btn.disabled = false;
|
|
1942
|
-
|
|
2032
|
+
// **何も変わっていないなら「追加した」と見せない。** 既に同じものが入っていた場合
|
|
2033
|
+
// サーバは 200 + changed:false を返す。ここを res.ok だけで判定すると
|
|
2034
|
+
// 「変更はありません」と言いながら追加した行が出る
|
|
2035
|
+
if (onDone && j.changed !== false) onDone();
|
|
1943
2036
|
} catch (e) {
|
|
1944
2037
|
msg.className = "ds-toggle-msg is-error";
|
|
1945
2038
|
msg.textContent = "✗ 通信に失敗しました。";
|
|
@@ -2040,20 +2133,37 @@ const CLIENT_JS = `
|
|
|
2040
2133
|
return;
|
|
2041
2134
|
}
|
|
2042
2135
|
if (opts.confirmLines && !confirmDanger(opts.confirmLines(main.value.trim()))) { msg.textContent = ""; return; }
|
|
2043
|
-
var
|
|
2136
|
+
var added = main.value.trim();
|
|
2137
|
+
var addedName = name ? name.value.trim() : "";
|
|
2138
|
+
var payload = opts.build(added, addedName);
|
|
2044
2139
|
sendConfig(payload, msg, btn, function () {
|
|
2045
2140
|
// 追加できたら入力欄を空にする(同じものを二度押ししないように)
|
|
2046
2141
|
main.value = "";
|
|
2047
2142
|
if (name) name.value = "";
|
|
2143
|
+
// **追加したものをその場に出す。** 上の一覧は fleet-status.json(夜間スナップショット)
|
|
2144
|
+
// から描かれているので、追加してもその場では変わらない。何も変わらないと
|
|
2145
|
+
// 「追加できていない」と読まれ、もう一度押して「既に登録されています」になる
|
|
2146
|
+
// (実際にそうなった)。だから押した結果をここで見せる。
|
|
2147
|
+
pending.appendChild(
|
|
2148
|
+
el("div", { class: "ds-pending-item" }, [
|
|
2149
|
+
el("span", { class: "ds-pending-mark", text: "+" }),
|
|
2150
|
+
el("span", { class: "ds-pending-name", text: addedName || added }),
|
|
2151
|
+
el("span", { class: "ds-pending-note", text: "追加済み・反映中" }),
|
|
2152
|
+
]),
|
|
2153
|
+
);
|
|
2048
2154
|
});
|
|
2049
2155
|
});
|
|
2050
2156
|
|
|
2157
|
+
// このセッションで追加したものを並べる場所(リロードすると消える=正式な一覧は上に出る)
|
|
2158
|
+
var pending = el("div", { class: "ds-pending" });
|
|
2159
|
+
|
|
2051
2160
|
var box = el("div", { class: "ds-addform" }, [
|
|
2052
2161
|
el("div", { class: "ds-addform-title", text: opts.title }),
|
|
2053
2162
|
opts.hint ? el("p", { class: "sec-how", text: opts.hint }) : null,
|
|
2054
2163
|
main,
|
|
2055
2164
|
name,
|
|
2056
2165
|
el("div", { class: "ds-toggle" }, [btn, msg]),
|
|
2166
|
+
pending,
|
|
2057
2167
|
]);
|
|
2058
2168
|
return box;
|
|
2059
2169
|
}
|
package/package.json
CHANGED