@takagaki/cortex-decisions-viewer 0.12.177 → 0.12.180

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.
File without changes
package/dist/build.js CHANGED
@@ -36,8 +36,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.REFERENCES_SECTIONS = exports.RELATED_MATERIALS_SECTIONS = exports.RELATED_URL_MAX = exports.RELATED_MATERIALS_FIELD = exports.DECISION_FIRMNESS = exports.GRAPH_RELS = exports.GRAPH_RECORD_TYPES = exports.PROJECT_ICON_EXTS = exports.MIRROR_RELS = void 0;
39
+ exports.REFERENCES_SECTIONS = exports.RELATED_MATERIALS_SECTIONS = exports.RELATED_URL_MAX = exports.RELATED_MATERIALS_FIELD = exports.DECISION_FIRMNESS = exports.GRAPH_RELS = exports.GRAPH_RECORD_TYPES = exports.GUIDE_VIDEO_SOURCES = exports.PROJECT_ICON_EXTS = exports.MIRROR_RELS = void 0;
40
40
  exports.sanitizeBody = sanitizeBody;
41
+ exports.copyHashedAsset = copyHashedAsset;
42
+ exports.buildGuideAssets = buildGuideAssets;
41
43
  exports.findProjectIcon = findProjectIcon;
42
44
  exports.readFleetSources = readFleetSources;
43
45
  exports.orderRecordsByLedgers = orderRecordsByLedgers;
@@ -299,6 +301,58 @@ function resolveKnowledgeRoot(decisionsDirAbs) {
299
301
  * `.jpeg` は入れない(前の intake は `jpg` へ正規化して書いていた・拾うと「どちらが正か」が2つになる)。
300
302
  */
301
303
  exports.PROJECT_ICON_EXTS = ["png", "jpg", "webp"];
304
+ /**
305
+ * 手順動画の置き場(#1353)。**今はまだ1件も無い**——npm パッケージに
306
+ * `viewer/assets/guides/<連携の種別>.mp4` を同梱したら、ここに1行足すだけで配信が始まる
307
+ * (npm の next → カナリア確認 → latest 昇格 → 各案件の次回ビルド、という既存の画像と
308
+ * 同じ広がり方。段階的に全案件へ届く)。キーは SECRET_FORMS 等で使う連携の種別(kind)。
309
+ *
310
+ * **絶対パスは `__dirname`(`dist/build.js`)から2つ上=パッケージルートの `assets/guides/`
311
+ * を指す。** `package.json` の `files` にも `assets` を加えてある(dist と同じ階層で配布される)。
312
+ */
313
+ exports.GUIDE_VIDEO_SOURCES = {
314
+ // 例: backlog: { video: path.join(__dirname, "..", "assets", "guides", "backlog.mp4") },
315
+ };
316
+ /**
317
+ * ファイルを内容ハッシュ名で `outAbs/assets/` へコピーする。CSS/JS と同じ規約
318
+ * (sha256 先頭8桁・`--cache-control public, max-age=31536000, immutable` で配信される
319
+ * `assets/` 配下)に動画も乗せる。**固定名にしないのは、動画の内容を変えても配信先に
320
+ * 古いバイト列が残ったまま長期キャッシュされる事故を防ぐため**(#1353 設計1.3節)。
321
+ *
322
+ * 読めない(まだ無い等)ときは undefined。**ビルドは止めない**——その連携の guide 宣言は
323
+ * 「動画なし」の表示に倒れる(動画ファイルが無い今の全案件がそうなる)。
324
+ */
325
+ async function copyHashedAsset(outAbs, srcAbsPath, namePrefix) {
326
+ let bytes;
327
+ try {
328
+ bytes = await node_fs_1.promises.readFile(srcAbsPath);
329
+ }
330
+ catch {
331
+ return undefined;
332
+ }
333
+ const hash = (0, node_crypto_1.createHash)("sha256").update(bytes).digest("hex").slice(0, 8);
334
+ const fileExt = path.extname(srcAbsPath).replace(/^\./, "") || "bin";
335
+ const fileName = `${namePrefix}.${hash}.${fileExt}`;
336
+ await node_fs_1.promises.mkdir(path.join(outAbs, "assets"), { recursive: true });
337
+ await node_fs_1.promises.writeFile(path.join(outAbs, "assets", fileName), bytes);
338
+ return `assets/${fileName}`;
339
+ }
340
+ /**
341
+ * 連携の種別ごとの手順動画を `outAbs/assets/` へ写し、サイト内相対パスの表を返す。
342
+ * 1件も無ければ undefined(SiteData の「空の器は通さない」規約・`projectIcon` 等と同じ扱い)。
343
+ */
344
+ async function buildGuideAssets(outAbs, sources) {
345
+ const out = {};
346
+ for (const kind of Object.keys(sources)) {
347
+ const videoAbsPath = sources[kind].video;
348
+ if (!videoAbsPath)
349
+ continue;
350
+ const url = await copyHashedAsset(outAbs, videoAbsPath, `guide-${kind}`);
351
+ if (url)
352
+ out[kind] = { video: url };
353
+ }
354
+ return Object.keys(out).length ? out : undefined;
355
+ }
302
356
  /**
303
357
  * `Cortex/icon.<ext>` を探す。無ければ undefined(キーごと出さないため)。
304
358
  *
@@ -2638,6 +2692,9 @@ async function buildSite(opts, views) {
2638
2692
  }
2639
2693
  if (projectIcon)
2640
2694
  await node_fs_1.promises.copyFile(projectIcon.srcAbs, path.join(outAbs, projectIcon.file));
2695
+ // 手順動画(#1353)。GUIDE_VIDEO_SOURCES が空の今は、1件も書き出されず site.guideAssets も
2696
+ // undefined のまま(キーごと省略・空の器は通さない)——今のどの案件の表示も変わらない
2697
+ site.guideAssets = await buildGuideAssets(outAbs, exports.GUIDE_VIDEO_SOURCES);
2641
2698
  let thumbIndex = 0;
2642
2699
  for (const node of graph.nodes) {
2643
2700
  if (!node.thumb)
package/dist/render.js CHANGED
@@ -5123,12 +5123,6 @@ a.pager-btn:hover { text-decoration: none; }
5123
5123
  .li-basis.is-firm { color: #15803d; }
5124
5124
  .li-basis.is-machine { color: var(--muted); }
5125
5125
  .li-basis.is-plain { color: var(--text); }
5126
- /* 否定済み(not_aliases)の畳み。つながっている側より一段控えめに(#704) */
5127
- .li-denied { margin-top: 10px; font-size: 13px; }
5128
- .li-denied-summary { cursor: pointer; font-size: 12.5px; color: var(--muted); }
5129
- .li-denied-list { margin: 6px 0 0; padding-left: 20px; }
5130
- .li-denied-row { overflow-wrap: anywhere; color: var(--muted); }
5131
- .li-denied-kind { display: inline-block; min-width: 72px; margin-right: 8px; font-weight: 700; }
5132
5126
  /* 違っていたときにどうするかの断り。.ch-note と同じ置き方(節の末尾の小さな段落) */
5133
5127
  .li-note { font-size: 12px; color: var(--muted); line-height: 1.7; margin: 12px 0 0; }
5134
5128
  /* ---- レコードの削除(?op=delete-record)----
@@ -5857,6 +5851,25 @@ a.ds-tile:hover { text-decoration: none; }
5857
5851
  Google Chat の 2 枚がモーダルの内側幅(約 996px)まで**引き伸ばされてぼける**
5858
5852
  ——「原寸の幅に戻す」と言いながら原寸より大きく出すことになる */
5859
5853
  .mt-shot-full { max-width: 100%; width: auto; cursor: zoom-out; }
5854
+ /* 手順動画+画像のカルーセル(#1353)。buildGuideCarousel が作る唯一の口。
5855
+ 🔴 **ここでも幅はこの1か所だけ**(.mt-shot と同じ640pxに揃える)。表示切替は
5856
+ hidden属性のトグルだけで行う(CSSOMのstyleプロパティへの代入はしない)。
5857
+ スライドの高さは揃えない——固定枠にして object-fit で収めると、動画(16:9)と
5858
+ 縦長のスクリーンショットが混在したときの比率計算にstyle操作が要ることになり、
5859
+ CSPの検査が拾いにくい書き方を増やす(実装レビュー指摘)。各スライドは
5860
+ .mt-shot / .mt-carousel-video と同じ「width:100%; height:auto」に任せ、
5861
+ 枚数を切り替えるたびに高さが変わる(ガタつきはあるが、CSP安全性を優先する) */
5862
+ .mt-carousel { margin: 6px 0 0; max-width: 640px; }
5863
+ .mt-carousel-slide[hidden] { display: none; }
5864
+ .mt-carousel-slide .mt-shot { margin: 0; }
5865
+ .mt-carousel-video { display: block; width: 100%; height: auto; margin: 0; border: 1px solid var(--border); border-radius: 6px; background: #000; }
5866
+ .mt-carousel-nav { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-top: 6px; }
5867
+ .mt-carousel-arrow { display: inline-flex; align-items: center; justify-content: center; width: 30px; height: 30px; font-size: 16px; line-height: 1; padding: 0; background: var(--surface); border: 1px solid var(--border); border-radius: 6px; color: var(--text); cursor: pointer; }
5868
+ .mt-carousel-arrow:hover { background: var(--border); }
5869
+ .mt-carousel-dots { display: flex; gap: 6px; }
5870
+ .mt-carousel-dot { width: 8px; height: 8px; padding: 0; border: none; border-radius: 50%; background: var(--border); cursor: pointer; }
5871
+ .mt-carousel-dot.is-active { background: var(--accent); }
5872
+ .mt-carousel-caption { margin: 6px 0 0; font-size: 12.5px; color: var(--muted); }
5860
5873
  /* 0件を黙って見せない(追加フォームだけだと「未登録」か「読めていない」か区別できない) */
5861
5874
  .ds-empty { color: var(--muted); font-size: 12.5px; margin: 4px 0 10px; }
5862
5875
  .sec-input { width: 100%; box-sizing: border-box; font-size: 13px; padding: 7px 10px; margin-bottom: 8px; border: 1px solid var(--border); border-radius: 6px; background: #fff; color: var(--text); }
@@ -6768,6 +6781,139 @@ ${story_1.splitByNameLinks.toString()}
6768
6781
  });
6769
6782
  return img;
6770
6783
  }
6784
+ // 手順動画+画像のカルーセル(#1353)を作る**唯一の口**。guide が無ければ null
6785
+ // (呼び出し側は null を見て何も差し込まない。空の枠を出さない)。
6786
+ //
6787
+ // guide = { video?: { alt, poster? }, shots: [{ src, width, height, alt, heading? }, ...] }
6788
+ // - video の実 URL は guide には置かない。**SITE.guideAssets[kind].video**(build.ts が
6789
+ // 動画ファイルを site/assets/ へ写したときだけ注入する)から取る。guide.video があっても
6790
+ // SITE.guideAssets[kind].video が無ければ動画スライドを出さない——動画ファイルが
6791
+ // まだ無い今のすべての案件で、現状の表示から何も変わらないことの土台(#1353 実装設計1節)
6792
+ // - guide.video.poster … 動画の再生前に出す静止画。画像(shots)と同じ data URI で埋め込む
6793
+ // (配るのは動画1本だけ。ポスターは宣言側が持つ)。あれば <video poster> に付ける
6794
+ // - shots は今の meta.shots と同じ形。画像は data URI 埋め込みのまま(変更なし)
6795
+ //
6796
+ // 🔴 **ここでも幅の数字を呼び出し側に書かせない**(CSS の .mt-carousel 1 か所だけ)。
6797
+ // ⚠ **表示切替は hidden 属性(DOM プロパティ代入)だけ**。インラインの style を書き込む
6798
+ // 手段(CSP の検査・csp-compatible.test.mjs が禁じる書き方)は使わない。
6799
+ // ⚠ ハンドラは全部 addEventListener(インラインの on* 属性は配信先の CSP が実行しない)。
6800
+ // ⚠ guideShot 側の注意(直前のコメント)と同じ理由で、受け皿の変数名に s / p / src / ext を
6801
+ // 使わない(fleet-passthrough の走査の誤検知を避ける)。属性を組み立てる el() への
6802
+ // プロパティキー("src": ...)は対象外——シグネチャ上避けられないHTML属性名そのもの。
6803
+ function buildGuideCarousel(kind, guide) {
6804
+ if (!guide) return null;
6805
+ var asset = (SITE.guideAssets && SITE.guideAssets[kind]) || null;
6806
+ var slides = [];
6807
+ if (guide.video && asset && asset.video) {
6808
+ var videoAttrs = {
6809
+ class: "mt-carousel-video",
6810
+ src: asset.video,
6811
+ preload: "none",
6812
+ controls: "",
6813
+ playsinline: "",
6814
+ "aria-label": guide.video.alt || "",
6815
+ };
6816
+ if (guide.video.poster) videoAttrs.poster = guide.video.poster;
6817
+ slides.push({
6818
+ node: el("video", videoAttrs),
6819
+ caption: guide.video.alt || "",
6820
+ });
6821
+ }
6822
+ (guide.shots || []).forEach(function (shot) {
6823
+ slides.push({
6824
+ node: guideShot({ src: shot.src, width: shot.width, height: shot.height, alt: shot.alt }),
6825
+ caption: shot.heading || shot.alt || "",
6826
+ });
6827
+ });
6828
+ if (!slides.length) return null;
6829
+
6830
+ var viewport = el("div", { class: "mt-carousel-viewport" });
6831
+ var slideEls = slides.map(function (slide) {
6832
+ // data-caption … 並び順の見出し/altをDOMから直接読めるようにする(画面には出さない)。
6833
+ // 画面表示は .mt-carousel-caption の textContent(現在のスライドだけ)に任せる
6834
+ var wrap = el("div", { class: "mt-carousel-slide", "data-caption": slide.caption }, [slide.node]);
6835
+ viewport.appendChild(wrap);
6836
+ return wrap;
6837
+ });
6838
+ var caption = el("div", { class: "mt-carousel-caption", "aria-live": "polite" });
6839
+ var root = el("div", {
6840
+ class: "mt-carousel",
6841
+ role: "group",
6842
+ "aria-roledescription": "carousel",
6843
+ tabindex: "0",
6844
+ }, [viewport]);
6845
+
6846
+ var current = 0;
6847
+ var dotEls = [];
6848
+ function show(i) {
6849
+ current = ((i % slideEls.length) + slideEls.length) % slideEls.length;
6850
+ slideEls.forEach(function (slideEl, idx) {
6851
+ var hide = idx !== current;
6852
+ // 隠した動画は display:none でも音が鳴り続けるので止める
6853
+ if (hide && !slideEl.hidden) {
6854
+ var playing = slideEl.querySelector("video");
6855
+ if (playing && typeof playing.pause === "function") playing.pause();
6856
+ }
6857
+ slideEl.hidden = hide;
6858
+ });
6859
+ // フォーカスが隠れたスライドの中にあったら外枠へ戻す(キーボード操作が途切れない)
6860
+ if (document.activeElement && document.activeElement !== root && viewport.contains(document.activeElement)
6861
+ && slideEls[current] && !slideEls[current].contains(document.activeElement)) root.focus();
6862
+ caption.textContent = slides[current].caption;
6863
+ dotEls.forEach(function (dotEl, idx) {
6864
+ dotEl.classList.toggle("is-active", idx === current);
6865
+ if (idx === current) dotEl.setAttribute("aria-current", "true"); else dotEl.removeAttribute("aria-current");
6866
+ });
6867
+ }
6868
+
6869
+ // 1枚だけなら矢印・ドットなしの素の枠にする(枚数が増えない限り視覚的ノイズを足さない)
6870
+ if (slides.length > 1) {
6871
+ var prev = el("button", { type: "button", class: "mt-carousel-arrow", "aria-label": "前の手順", text: "‹" });
6872
+ var next = el("button", { type: "button", class: "mt-carousel-arrow", "aria-label": "次の手順", text: "›" });
6873
+ var dots = el("div", { class: "mt-carousel-dots" });
6874
+ slides.forEach(function (_slide, idx) {
6875
+ var dot = el("button", { type: "button", class: "mt-carousel-dot", "aria-label": (idx + 1) + "枚目を表示" });
6876
+ dot.addEventListener("click", function () { show(idx); });
6877
+ dotEls.push(dot);
6878
+ dots.appendChild(dot);
6879
+ });
6880
+ prev.addEventListener("click", function () { show(current - 1); });
6881
+ next.addEventListener("click", function () { show(current + 1); });
6882
+ root.appendChild(el("div", { class: "mt-carousel-nav" }, [prev, dots, next]));
6883
+ // キーボード送り(カルーセルにフォーカスがあるとき)
6884
+ root.addEventListener("keydown", function (e) {
6885
+ // 動画のコントロール上の ←→(ネイティブのシーク)と、ブラウザの戻る系ショートカットは奪わない
6886
+ if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return;
6887
+ if (e.target && e.target.tagName === "VIDEO") return;
6888
+ if (e.key === "ArrowLeft") { e.preventDefault(); show(current - 1); }
6889
+ else if (e.key === "ArrowRight") { e.preventDefault(); show(current + 1); }
6890
+ });
6891
+ // スワイプ(タッチ)。横移動が小さいときは無視する(縦スクロールの誤爆を防ぐ)
6892
+ var touchStartX = null;
6893
+ var touchStartY = null;
6894
+ viewport.addEventListener("touchstart", function (e) {
6895
+ // 動画のシークバーの横ドラッグはスワイプにしない。複数指も対象外
6896
+ var single = e.touches && e.touches.length === 1 && !(e.target && e.target.tagName === "VIDEO");
6897
+ touchStartX = single ? e.touches[0].clientX : null;
6898
+ touchStartY = single ? e.touches[0].clientY : null;
6899
+ }, { passive: true });
6900
+ viewport.addEventListener("touchcancel", function () { touchStartX = null; touchStartY = null; }, { passive: true });
6901
+ viewport.addEventListener("touchend", function (e) {
6902
+ if (touchStartX === null) return;
6903
+ var endX = e.changedTouches && e.changedTouches.length ? e.changedTouches[0].clientX : touchStartX;
6904
+ var endY = e.changedTouches && e.changedTouches.length ? e.changedTouches[0].clientY : touchStartY;
6905
+ var dx = endX - touchStartX;
6906
+ var dy = endY - touchStartY;
6907
+ touchStartX = null;
6908
+ touchStartY = null;
6909
+ if (Math.abs(dx) < 30 || Math.abs(dy) >= Math.abs(dx)) return;
6910
+ if (dx < 0) show(current + 1); else show(current - 1);
6911
+ }, { passive: true });
6912
+ }
6913
+ root.appendChild(caption);
6914
+ show(0);
6915
+ return root;
6916
+ }
6771
6917
  // 未確認(status: draft = AI生成・人間未確認)の印。Gold層の4型(決定事項・用語集・メンバー・ルール)共通で、active・status無しには何も出さない
6772
6918
  function draftBadge(rec) {
6773
6919
  return rec && unconfirmed(rec) ? el("span", { class: "badge draft", text: "AI生成・未確認" }) : null;
@@ -6909,14 +7055,6 @@ ${story_1.splitByNameLinks.toString()}
6909
7055
  el("span", { text: "この記録を確認済みにしました。一覧などの表示は、数分後に画面が作り直されたときに反映されます。" }),
6910
7056
  ]), note);
6911
7057
  }
6912
- /**
6913
- * 画面に出す提案(**答え済みの名寄せの提案を除く**・#704)。
6914
- *
6915
- * 🔴 claim_key の ID が、記録の identities(同じ人と答えた)か not_aliases(別人と答えた)に
6916
- * もう在るなら、そのカードは**出さない**——従来のカードにも落とさない。人が答えたあと、
6917
- * engine がカードを取り下げるまでの夜(最長 14 日)に同じ問いを出し続けると、
6918
- * [別人です]を押し直して宣言した行を黙って切る形になる
6919
- */
6920
7058
 
6921
7059
  // 状態の絞り込み。未確認のみ(draft)の選択肢だけを持つ
6922
7060
  function statusLabel(v) {
@@ -15512,6 +15650,22 @@ ${story_1.splitByNameLinks.toString()}
15512
15650
  build: function (url) {
15513
15651
  return { op: "backlog-target-add", url: url, baseGeneration: targetsGenerationOf(targets) };
15514
15652
  },
15653
+ // **ほかのプロジェクトでも使われているプロジェクトキーの確認(#1354)。** 画面から接続先を足すのはこのフォームだけ
15654
+ //(set-backlog-target を送るフォームは無い)。振り分け表はスペースを持たないので「同じプロジェクトキー」と言う。
15655
+ // 相手のプロジェクト名は出さない(この画面の操作は認証を通れる人なら誰でもできる)
15656
+ duplicateConfirmLines: function (value) {
15657
+ return [
15658
+ "同じプロジェクトキーが、ほかのプロジェクトでも使われています。",
15659
+ "",
15660
+ "このまま追加すると、同じ課題・ドキュメント・Wiki が、このプロジェクトのリポジトリにも取り込まれます。",
15661
+ "",
15662
+ "別のBacklogプロジェクトと取り違えていないか、貼ったURLを確かめてください:",
15663
+ String(value || ""),
15664
+ "",
15665
+ "追加しますか?",
15666
+ ];
15667
+ },
15668
+ duplicateKind: "backlog",
15515
15669
  // 応答の一覧で本物の行が増えるので、フォーム側の控え(「追加済み・反映中」)は出さない
15516
15670
  hooks: targetsHooks,
15517
15671
  }));
@@ -16009,6 +16163,20 @@ ${story_1.splitByNameLinks.toString()}
16009
16163
  // 手順を置いていたが、トークンの発行手順の正本は鍵の欄(SECRET_FORMS.figma の how・note・画像)、
16010
16164
  // 読めないファイルの一手は行の中(DS_GATE_CAP.design)、「何が同期されるか」は上部の説明文が持つ。
16011
16165
  build: function (url, name) { return { op: "add-figma-file", url: url, name: name }; },
16166
+ // 別のプロジェクトでも追加されているファイルの確認(#1354)
16167
+ duplicateConfirmLines: function (value) {
16168
+ return [
16169
+ "このFigmaファイルは、別のプロジェクトでも追加されています。",
16170
+ "",
16171
+ "このまま追加すると、同じファイルのデザインが、このプロジェクトのリポジトリにも取り込まれます。",
16172
+ "",
16173
+ "別のファイルと取り違えていないか、貼ったURLを確かめてください:",
16174
+ String(value || ""),
16175
+ "",
16176
+ "追加しますか?",
16177
+ ];
16178
+ },
16179
+ duplicateKind: "figma",
16012
16180
  }));
16013
16181
  used = true;
16014
16182
  }
@@ -16082,6 +16250,7 @@ ${story_1.splitByNameLinks.toString()}
16082
16250
  "追加しますか?",
16083
16251
  ];
16084
16252
  },
16253
+ duplicateKind: "slack",
16085
16254
  adder: "slack",
16086
16255
  }));
16087
16256
 
@@ -16093,7 +16262,7 @@ ${story_1.splitByNameLinks.toString()}
16093
16262
  // **接続(OAuth)のボタンは無い。** 顧客の Workspace 管理者による Admin install が
16094
16263
  // 承認そのもので、画面に押す先が無い(Slack の接続ボタンを写経しないこと——
16095
16264
  // 押しても何も起きないボタンが顧客の画面に出る)。
16096
- // **追加時の確認モーダルも出さない**(#167 で「追加のたびの確認」は廃止済み。
16265
+ // **追加のたびの確認モーダルは出さない**(#167 で廃止済み。別のプロジェクトと重なったときの確認だけ出る・#1354。
16097
16266
  // 「追加=Gold 昇格の対象」という重さは、モーダル上部の説明文が 1 回だけ言う)。
16098
16267
  // Google Chat アプリの表示名は GCHAT_APP_NAME(Marketplace 公開名・#387)。
16099
16268
  box.appendChild(addForm({
@@ -16123,6 +16292,20 @@ ${story_1.splitByNameLinks.toString()}
16123
16292
  // 読めなければ足した行の中が理由に合った一手を出す(githubRowHelpParts)。
16124
16293
  // 「毎晩の記録に入る・決定と用語の材料にはしない」の説明は、モーダル上部の説明文が 1 回だけ持つ。
16125
16294
  build: function (repo, name) { return { op: "add-github-source", repo: repo, name: name }; },
16295
+ // 別のプロジェクトでも追加されているリポジトリの確認(#1354)
16296
+ duplicateConfirmLines: function (value) {
16297
+ return [
16298
+ "このリポジトリは、別のプロジェクトでも追加されています。",
16299
+ "",
16300
+ "このまま追加すると、同じリポジトリの内容が、このプロジェクトの毎晩の記録にも入ります。",
16301
+ "",
16302
+ "別のリポジトリと取り違えていないか、入れた値を確かめてください:",
16303
+ String(value || ""),
16304
+ "",
16305
+ "追加しますか?",
16306
+ ];
16307
+ },
16308
+ duplicateKind: "github",
16126
16309
  adder: "github",
16127
16310
  }));
16128
16311
  }
@@ -16253,6 +16436,12 @@ ${story_1.splitByNameLinks.toString()}
16253
16436
  * 失敗(エラー応答・通信失敗・何も変わらなかった)では仮置きを**巻き戻して**理由を出す。
16254
16437
  * 仮置きを作れない操作(切り出せない入力・仮置きの対象でない op)は、従来どおり待ち表示を出して応答を待つ。
16255
16438
  */
16439
+ /*
16440
+ * 重なりを確認すれば覆せる種別(2026-08-20 Slack・#1354 で 4 種別。Drive・Google Chat は中央のアカウントで読むので含めない)。綴りと並びは intake の
16441
+ * CONFIRMABLE_DUPLICATE_KINDS(infra/lambda/intake/logic.mjs)と完全一致させる(infra/test/duplicate-verdict.test.mjs が写しの一致を見る)。
16442
+ * サーバの印がこの許可リストに無い種別なら、確認は出さない(truthy で見ない)
16443
+ */
16444
+ var CONFIRMABLE_KINDS = ["slack", "figma", "backlog", "github"];
16256
16445
  async function sendConfig(payload, msg, btn, onDone, hooks) {
16257
16446
  var intake = SITE.intake;
16258
16447
  if (!intake) return;
@@ -16308,11 +16497,16 @@ ${story_1.splitByNameLinks.toString()}
16308
16497
  // **サーバを切り戻したとき**と**デプロイ中の版ズレ**。どちらも確認済みの再送だけが
16309
16498
  // 断られる状態で、そこで出し続けると「はい」を押し続けることになる。
16310
16499
  // 配る順番はサーバ→画面(これは順序の話とは別に必要)。
16500
+ //
16501
+ // **種別は許可リストの完全一致(#1354)**、さらに**そのフォームの種別と一致**するときだけ出す
16502
+ // (Drive の追加フォームに Slack の印が返っても、Drive の確認文で覆さない)。
16311
16503
  var canConfirm =
16312
16504
  res.status === 409 &&
16313
16505
  j.confirmable === "duplicate" &&
16314
- j.confirmableKind === "slack" &&
16506
+ typeof j.confirmableKind === "string" &&
16507
+ CONFIRMABLE_KINDS.indexOf(j.confirmableKind) >= 0 &&
16315
16508
  hooks &&
16509
+ j.confirmableKind === hooks.duplicateKind &&
16316
16510
  typeof hooks.onDuplicate === "function" &&
16317
16511
  payload.confirmDuplicate !== true;
16318
16512
  if (canConfirm) {
@@ -16761,9 +16955,9 @@ ${story_1.splitByNameLinks.toString()}
16761
16955
  );
16762
16956
  }, (function () {
16763
16957
  // **既存の hooks(接続先の一覧が持つ onTargets)と、重複の確認を1つにまとめる。**
16764
- // Slack の追加フォームは onTargets を持たないが、重複の確認は要る。
16765
- // 逆に接続先の一覧(Backlog)は onTargets を持つが確認は要らない——
16766
- // 共通に足すと、あちらの「他の人が先に変えていた」の判定と混ざる。
16958
+ // Slack などの追加フォームは onTargets を持たないが、重複の確認は要る。
16959
+ // 接続先の一覧(Backlog)は onTargets も重複の確認も持つ(#1354)。確認は印(confirmable: "duplicate")の
16960
+ // 完全一致でしか出ないので、あちらの「他の人が先に変えていた」(世代のずれ)の判定とは混ざらない。
16767
16961
  var h = opts.hooks ? opts.hooks() : null;
16768
16962
  // **送信した瞬間に入力欄を空にし(楽観更新・#968 E)、断られたら元の値へ戻す**(打ち直させない)
16769
16963
  var sentMain = "";
@@ -16797,6 +16991,8 @@ ${story_1.splitByNameLinks.toString()}
16797
16991
  returnTo: btn,
16798
16992
  });
16799
16993
  };
16994
+ // このフォームの種別。サーバの印の種別と一致したときだけ確認を出す(sendConfig の canConfirm)
16995
+ h.duplicateKind = opts.duplicateKind;
16800
16996
  return h;
16801
16997
  })());
16802
16998
  });
@@ -18036,27 +18232,30 @@ ${story_1.splitByNameLinks.toString()}
18036
18232
  // 現場で切り分けに時間がかかる(実際に踏んだ)。入力欄の隣に書いておく。
18037
18233
  note: "発行したアカウントを、対象の Figma ファイルに閲覧者として招待しておいてください(招待が無いとトークンが正しくても読めません)。"
18038
18234
  + "発行するのは、対象ファイルのあるチームで Dev または Full の席を持つ方にしてください。閲覧席(View/Collab)のトークンは取得回数が月 20 回までで、夜間同期が数日で止まります。",
18039
- // **手順の画像3枚(#547)。** Google Meet・Google Chat と同じ guideShot を通す
18040
- // (幅の上限・クリックで原寸に戻る振る舞いを1か所に揃える)。見出しは画像の並び順
18041
- // (1〜3枚目)、画像の中の丸数字(①〜⑧)は1枚の中の細かい手順——役割が別なので、
18042
- // 見出し側に丸数字を混ぜない
18043
- shots: [
18044
- {
18045
- src: FIGMA_SHOT_STEP1, width: "1000", height: "1114",
18046
- heading: "1. 左上の名前 → Settings",
18047
- alt: "Figma左上の自分の名前を押すとメニューが開き、その中のSettingsが赤枠で示されている。",
18048
- },
18049
- {
18050
- src: FIGMA_SHOT_STEP2, width: "1200", height: "1145",
18051
- heading: "2. Security タブ → Generate new token を押す",
18052
- alt: "Settings画面のSecurityタブが赤枠で示され、下のPersonal access tokensにあるGenerate new tokenのリンクも赤枠で示されている。",
18053
- },
18054
- {
18055
- src: FIGMA_SHOT_STEP3, width: "1100", height: "1428",
18056
- heading: "3. 名前・期限・スコープを決めて発行する",
18057
- alt: "Generate new tokenダイアログ。Token name欄・Expiration(90日)・file_comments:readとfile_content:readのチェックボックス・Generate tokenボタンが、それぞれ赤枠で示されている。",
18058
- },
18059
- ],
18235
+ // **手順の画像3枚(#547)。** buildGuideCarousel(#1353)を通す1枚ずつ送りの枠に乗せる。
18236
+ // 見出しは画像の並び順(1〜3枚目)、画像の中の丸数字(①〜⑧)は1枚の中の細かい手順
18237
+ // ——役割が別なので、見出し側に丸数字を混ぜない。
18238
+ // 動画は無し(guide.video を省略)。将来動画を足すときは、この guide にそのまま
18239
+ // video: { alt } を足すだけで済む(SITE.guideAssets.figma.video が注入された日から配信開始)
18240
+ guide: {
18241
+ shots: [
18242
+ {
18243
+ src: FIGMA_SHOT_STEP1, width: "1000", height: "1114",
18244
+ heading: "1. 左上の名前 → Settings",
18245
+ alt: "Figma左上の自分の名前を押すとメニューが開き、その中のSettingsが赤枠で示されている。",
18246
+ },
18247
+ {
18248
+ src: FIGMA_SHOT_STEP2, width: "1200", height: "1145",
18249
+ heading: "2. Security タブ → Generate new token を押す",
18250
+ alt: "Settings画面のSecurityタブが赤枠で示され、下のPersonal access tokensにあるGenerate new tokenのリンクも赤枠で示されている。",
18251
+ },
18252
+ {
18253
+ src: FIGMA_SHOT_STEP3, width: "1100", height: "1428",
18254
+ heading: "3. 名前・期限・スコープを決めて発行する",
18255
+ alt: "Generate new tokenダイアログ。Token name欄・Expiration(90日)・file_comments:readとfile_content:readのチェックボックス・Generate tokenボタンが、それぞれ赤枠で示されている。",
18256
+ },
18257
+ ],
18258
+ },
18060
18259
  },
18061
18260
  "backlog": {
18062
18261
  icon: "📋",
@@ -18297,9 +18496,15 @@ ${story_1.splitByNameLinks.toString()}
18297
18496
  // 発行のしかたを探している人がここを開く手掛かりを持てない(rt カードと同じ流儀)
18298
18497
  var guidanceBox = el("details", { class: "sec-fold" }, [el("summary", { text: "発行のしかた" })]);
18299
18498
  for (var gi = 0; gi < guidance.length; gi++) guidanceBox.appendChild(guidance[gi]);
18300
- // **手順の画像(#547)。** 文章(guidance)の下に、見出し→画像の対を並びの順に足す。
18301
- // shots を持たない種別(Backlog)ではこの節は素通りする
18302
- if (meta.shots) {
18499
+ // **手順の動画+画像(#547・#1353)。** 文章(guidance)の下に足す。
18500
+ // meta.guide を持つ種別(Figma)はカルーセル1つに乗せる。meta.guide も meta.shots も
18501
+ // 持たない種別(Backlog)ではこの節は素通りする
18502
+ if (meta.guide) {
18503
+ var carousel = buildGuideCarousel(kind, meta.guide);
18504
+ if (carousel) guidanceBox.appendChild(carousel);
18505
+ } else if (meta.shots) {
18506
+ // 後方互換(旧形)。今のところ参照するエントリは無い——将来 Google Chat/Meet を
18507
+ // この口に載せ替えるまでの踏み板として残す
18303
18508
  for (var si = 0; si < meta.shots.length; si++) {
18304
18509
  var shot = meta.shots[si];
18305
18510
  guidanceBox.appendChild(el("p", { class: "sec-how", text: shot.heading }));
@@ -19542,10 +19747,7 @@ ${story_1.splitByNameLinks.toString()}
19542
19747
  // 数値 id で組むと開いた先に誰も居ない(または別人が出る)
19543
19748
  var LI_BACKLOG_USER = /^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/;
19544
19749
  var LI_BACKLOG_DOMAIN = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.backlog\\.(?:jp|com)$/i;
19545
- var LI_NOTE = "違うものがあれば「AIで編集」から「この ID は私ではない」と伝えると、翌晩から結ばなくなります。";
19546
- // 否定済み(not_aliases)の畳みの見出し。🔴 **中は値だけでリンクにしない**——
19547
- // 「私ではない」と言った先へ飛ばす口を出すと、つながっている側と見分けがつかなくなる
19548
- var LI_DENIED_TITLE = "これは私ではない(否定済み)";
19750
+ var LI_NOTE = "この ID があなたでない・この人でないときは、ポータルの人のページの ID 一覧で答えてください。";
19549
19751
 
19550
19752
  /** 種類 → 道具の読める名前。語彙の外はその綴りのまま */
19551
19753
  function liKindLabel(kind) {
@@ -19787,73 +19989,38 @@ ${story_1.splitByNameLinks.toString()}
19787
19989
  return out;
19788
19990
  }
19789
19991
  /**
19790
- * 否定済み(not_aliases)の行。**文字列の要素だけ**を、書かれた順のまま返す。
19791
- * <種類>:<値> の形なら道具の名前と値に割り、素の文字列はそのまま値として出す
19792
- */
19793
- function deniedIdRows(rec) {
19794
- var list = rec && rec.fm ? rec.fm.not_aliases : null;
19795
- if (!Array.isArray(list)) return [];
19796
- var out = [];
19797
- list.forEach(function (one) {
19798
- if (typeof one !== "string") return;
19799
- var whole = one.trim();
19800
- if (whole === "") return;
19801
- var typed = LI_TYPED_ID.exec(whole);
19802
- out.push({ kindLabel: typed ? liKindLabel(typed[1]) : "", value: typed ? typed[2] : whole });
19803
- });
19804
- return out;
19805
- }
19806
- /**
19807
- * 「つながっている ID」の折りたたみ。**関係者以外の記録と、つながっている ID も
19808
- * 否定済みも 0 件のときは null**(節ごと出さない=欄が無い記録は今までと同じに見える)。
19992
+ * 「つながっている ID」の折りたたみ。**関係者以外の記録と、つながっている ID が
19993
+ * 0 件のときは null**(節ごと出さない=欄が無い記録は今までと同じに見える)。
19809
19994
  *
19810
19995
  * 🔴 **表に table を使わない**(画面の部品に table は1つも無い)。.fm-grid と同じ
19811
19996
  * 書き方のグリッドで組む。値は全部 el() の text(textContent 経由)で入れる。
19812
- * 🔴 **否定済みは同じ節の中に、さらに畳んで出す**(開かなければ見えない)。
19813
- * つながっている ID が無く否定済みだけある記録にも節を出す——「前に否定したか」を
19814
- * 確かめる口が画面から消えないように
19997
+ * 🔴 **記録の not_aliases(否定済み)は読まない・画面に出さない**(member.no-email)。
19998
+ * 否定の答えを持つのはポータルで、見る・取り消すのはポータルの ID 一覧
19815
19999
  */
19816
20000
  function buildLinkedIds(rec) {
19817
20001
  if (!isMemberRec(rec)) return null;
19818
20002
  var rows = linkedIdRows(rec);
19819
- var denied = deniedIdRows(rec);
19820
- if (!rows.length && !denied.length) return null;
19821
- var head = "つながっている ID(" + rows.length + "件" + (denied.length ? "・否定済み " + denied.length + "件" : "") + ")";
20003
+ if (!rows.length) return null;
20004
+ var head = "つながっている ID(" + rows.length + "件)";
19822
20005
  var det = el("details", { class: "linked-ids" }, [
19823
20006
  el("summary", { class: "li-summary", text: head }),
19824
20007
  ]);
19825
- if (rows.length) {
19826
- var grid = el("div", { class: "li-grid" });
19827
- LI_COLS.forEach(function (col) {
19828
- grid.appendChild(el("div", { class: "li-cell li-head", text: col }));
19829
- });
19830
- rows.forEach(function (row) {
19831
- grid.appendChild(el("div", { class: "li-cell li-kind", text: row.kindLabel }));
19832
- var valueCell = el("div", { class: "li-cell li-value" });
19833
- valueCell.appendChild(row.href
19834
- ? el("a", { href: row.href, target: "_blank", rel: "noopener", text: row.value })
19835
- : el("span", { text: row.value }));
19836
- grid.appendChild(valueCell);
19837
- grid.appendChild(el("div", { class: "li-cell li-basis " + row.tone, text: row.basisText }));
19838
- grid.appendChild(el("div", { class: "li-cell li-since", text: row.since }));
19839
- grid.appendChild(el("div", { class: "li-cell li-scope", text: row.scope }));
19840
- });
19841
- det.appendChild(grid);
19842
- }
19843
- if (denied.length) {
19844
- var box = el("details", { class: "li-denied" }, [
19845
- el("summary", { class: "li-denied-summary", text: LI_DENIED_TITLE + "(" + denied.length + "件)" }),
19846
- ]);
19847
- var ul = el("ul", { class: "li-denied-list" });
19848
- denied.forEach(function (d) {
19849
- var li = el("li", { class: "li-denied-row" });
19850
- if (d.kindLabel) li.appendChild(el("span", { class: "li-denied-kind", text: d.kindLabel }));
19851
- li.appendChild(el("span", { class: "li-denied-value", text: d.value }));
19852
- ul.appendChild(li);
19853
- });
19854
- box.appendChild(ul);
19855
- det.appendChild(box);
19856
- }
20008
+ var grid = el("div", { class: "li-grid" });
20009
+ LI_COLS.forEach(function (col) {
20010
+ grid.appendChild(el("div", { class: "li-cell li-head", text: col }));
20011
+ });
20012
+ rows.forEach(function (row) {
20013
+ grid.appendChild(el("div", { class: "li-cell li-kind", text: row.kindLabel }));
20014
+ var valueCell = el("div", { class: "li-cell li-value" });
20015
+ valueCell.appendChild(row.href
20016
+ ? el("a", { href: row.href, target: "_blank", rel: "noopener", text: row.value })
20017
+ : el("span", { text: row.value }));
20018
+ grid.appendChild(valueCell);
20019
+ grid.appendChild(el("div", { class: "li-cell li-basis " + row.tone, text: row.basisText }));
20020
+ grid.appendChild(el("div", { class: "li-cell li-since", text: row.since }));
20021
+ grid.appendChild(el("div", { class: "li-cell li-scope", text: row.scope }));
20022
+ });
20023
+ det.appendChild(grid);
19857
20024
  // 違っていたときにどうするか。.ch-note と同じ置き方(節の末尾の小さな段落)
19858
20025
  det.appendChild(el("p", { class: "li-note", text: LI_NOTE }));
19859
20026
  return det;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takagaki/cortex-decisions-viewer",
3
- "version": "0.12.177",
3
+ "version": "0.12.180",
4
4
  "description": "Cortexの意思決定記録(Decisions/*.md)を静的サイトにビルドして閲覧する。各レコードに「Edit on GitHub」リンクを付与する。",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -12,7 +12,8 @@
12
12
  "cortex-decisions-viewer": "dist/cli.js"
13
13
  },
14
14
  "files": [
15
- "dist"
15
+ "dist",
16
+ "assets"
16
17
  ],
17
18
  "engines": {
18
19
  "node": ">=18"