@takagaki/cortex-decisions-viewer 0.12.160 → 0.12.163

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 CHANGED
@@ -36,7 +36,7 @@ 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.RELATED_MATERIALS_SECTIONS = exports.RELATED_URL_MAX = exports.RELATED_MATERIALS_FIELD = exports.DECISION_FIRMNESS = exports.PROJECT_ICON_EXTS = exports.MIRROR_RELS = void 0;
39
+ 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;
40
40
  exports.sanitizeBody = sanitizeBody;
41
41
  exports.findProjectIcon = findProjectIcon;
42
42
  exports.readFleetSources = readFleetSources;
@@ -287,15 +287,11 @@ function resolveKnowledgeRoot(decisionsDirAbs) {
287
287
  return null;
288
288
  }
289
289
  /**
290
- * 案件アイコン(#348)で受け付ける拡張子。**探索の順でもある**(2枚あれば先頭を使う)。
290
+ * 案件アイコン(#348)で拾う拡張子。**探索の順でもある**(2枚あれば先頭を使う)。
291
291
  *
292
- * **Lambda 側(infra/lambda/intake/logic.mjs の PROJECT_ICON_EXTS)と同じ並びにすること。**
293
- * 片方だけ増やす・並べ替えると、intake が書いた名前を build が拾わない(=設定したのに
294
- * 画面に出ない)か、2枚あるときに intake と build が別の1枚を選ぶ。
295
- * 突き合わせは viewer/test/project-icon-parity.test.mjs が機械的に見張る。
296
- *
297
- * `.jpeg` は入れない——intake は `.jpeg` を `jpg` へ正規化して書くので、
298
- * リポジトリにその名前は生まれない(拾うと「どちらが正か」が2つになる)。
292
+ * 置き場は案件リポの `Cortex/icon.<ext>`。画面と intake からアイコンを置く・外す口は外した(cortex-engine #1781・
293
+ * 仕様 common.screen-ops)ので、置くのは人が GitHub で直接。ポータルの取り込み(`v/icon.*`)も同じ 3 つを読む。
294
+ * `.jpeg` は入れない(前の intake は `jpg` へ正規化して書いていた・拾うと「どちらが正か」が2つになる)。
299
295
  */
300
296
  exports.PROJECT_ICON_EXTS = ["png", "jpg", "webp"];
301
297
  /**
@@ -1029,7 +1025,15 @@ function sanitizeFm(v) {
1029
1025
  }
1030
1026
  // グラフの凡例で独立した分類(=色)を持つ type。Gold 4区画(decision/term/member/rule)は
1031
1027
  // それぞれ独立の分類にする(「その他レコード」に丸めない——2026-08-28 オーナー裁定)
1032
- const GOLD_TYPES = new Set(["decision", "term", "member", "rule", "report", "overview"]);
1028
+ /** つながり画面の点にする記録の種類(仕様 9 章 graph.records-and-evidence)。Home・レポート・その他は点にしない */
1029
+ exports.GRAPH_RECORD_TYPES = new Set(["decision", "term", "member", "rule"]);
1030
+ /** 区画(置き場)の名前 → 点の種類。`type` の無い記録の受け皿 */
1031
+ const GRAPH_SECTION_TYPES = { Decisions: "decision", Glossary: "term", Members: "member", Rules: "rule" };
1032
+ /**
1033
+ * つながり画面の線にする relations の種類(仕様 9 章・記録どうしの frontmatter のつながり)。
1034
+ * 根拠・生成元・関連・置換と、根拠への参照・出典だけ。話題(about)など、機械が名前の一致で付けた手がかりは線にしない
1035
+ */
1036
+ exports.GRAPH_RELS = new Set(["based_on", "derived_from", "relates_to", "supersedes", "reference", "source"]);
1033
1037
  /** ディレクトリ配下の .md を再帰的に列挙する(存在しなければ空) */
1034
1038
  async function walkMd(dirAbs) {
1035
1039
  const out = [];
@@ -1680,7 +1684,7 @@ function attachGlow(nodes, glow, recordRelPaths) {
1680
1684
  * ほうがよい(`createRefResolver` と同じ考え方)。崩れた文字(宛先に見えない参照)も同様に、
1681
1685
  * 点は作るが飛び先は付けない。
1682
1686
  */
1683
- function buildGraph(decisions, home, sections, targetUrls, repoBaseUrl, book = (0, evidence_names_1.emptyEvidenceBook)()) {
1687
+ function buildGraph(decisions, sections, targetUrls, repoBaseUrl, book = (0, evidence_names_1.emptyEvidenceBook)()) {
1684
1688
  const nodes = new Map();
1685
1689
  const edges = [];
1686
1690
  const addNode = (node) => {
@@ -1692,11 +1696,14 @@ function buildGraph(decisions, home, sections, targetUrls, repoBaseUrl, book = (
1692
1696
  for (const d of decisions) {
1693
1697
  addNode({ id: d.id, label: d.title, group: "decision", recordId: d.id });
1694
1698
  }
1695
- if (home)
1696
- addNode({ id: home.id, label: home.title, group: "overview", recordId: home.id });
1699
+ // 🔴 点は決定・ルール・用語・関係者の記録と、その根拠だけ(仕様 graph.question / graph.records-and-evidence)。
1700
+ // Home・レポート・その他レコードは点にしない
1697
1701
  for (const sec of sections) {
1698
1702
  for (const r of sec.records) {
1699
- const group = GOLD_TYPES.has(r.rtype) ? r.rtype : "record";
1703
+ // 種類は type で決め、type の無い記録は置き場(区画)で決める。どちらでも 4 種類に当たらないものは点にしない
1704
+ const group = exports.GRAPH_RECORD_TYPES.has(r.rtype) ? r.rtype : GRAPH_SECTION_TYPES[sec.name];
1705
+ if (!group)
1706
+ continue;
1700
1707
  addNode({ id: r.id, label: r.title, group, recordId: r.id });
1701
1708
  }
1702
1709
  }
@@ -1704,11 +1711,10 @@ function buildGraph(decisions, home, sections, targetUrls, repoBaseUrl, book = (
1704
1711
  const fileNameToId = new Map();
1705
1712
  for (const d of decisions)
1706
1713
  fileNameToId.set(d.fileName, d.id);
1707
- if (home)
1708
- fileNameToId.set(home.fileName, home.id);
1709
1714
  for (const sec of sections)
1710
1715
  for (const r of sec.records)
1711
- fileNameToId.set(r.fileName, r.id);
1716
+ if (nodes.has(r.id))
1717
+ fileNameToId.set(r.fileName, r.id);
1712
1718
  // 🔴 **GitHubの写し(blob)へ飛ぶ点は作らない**(2026-10-01 オーナー決定・分類B)。
1713
1719
  // `targetUrls`(minute:/material:)や詳細画面と共用の解決器(RefResolver)は、元ツールの
1714
1720
  // URLが無いとき自分のリポジトリの blob URL に倒すことがある(詳細画面ではそれでよい設計)。
@@ -1749,6 +1755,8 @@ function buildGraph(decisions, home, sections, targetUrls, repoBaseUrl, book = (
1749
1755
  return ev.id;
1750
1756
  };
1751
1757
  // 参照先IDから層を推定してノード化する
1758
+ // 🔴 引けない宛先(記録でも根拠でもないもの)は点を作らない。数だけ数えて、ビルドのログに出す(黙って捨てない)
1759
+ let unresolvedTargets = 0;
1752
1760
  const classifyTarget = (target, humanLabel) => {
1753
1761
  if (nodes.has(target) && !labelRank.has(target))
1754
1762
  return target;
@@ -1759,30 +1767,44 @@ function buildGraph(decisions, home, sections, targetUrls, repoBaseUrl, book = (
1759
1767
  return resolved;
1760
1768
  if (target.startsWith(NO_ORIGINAL_TOOL_PREFIX)) {
1761
1769
  // 元ツールが無い根拠(engine #968 の将来の語彙)。リンクにせず、機械の語(urn:…)を出さない
1762
- return addNode({ id: target, label: NO_ORIGINAL_TOOL_LABEL, group: "phantom" });
1770
+ return addNode({ id: target, label: NO_ORIGINAL_TOOL_LABEL, group: "silver" });
1763
1771
  }
1764
1772
  const ev = identifyEvidence(target, identityCtx, humanLabel);
1765
1773
  if (ev)
1766
1774
  return addEvidence(ev);
1775
+ // 点にしない記録(Home・レポート・その他)へのパス参照は、根拠ではない。点を作らず数える
1776
+ if (/(^|\/)Cortex\/(Home\.md|[^/]+\/records\/)/.test(target)) {
1777
+ unresolvedTargets++;
1778
+ return null;
1779
+ }
1767
1780
  if (target.includes("/") || target.toLowerCase().endsWith(".md")) {
1768
1781
  // リポジトリ内ファイルへの生のパス参照で、帳(写しの課題・資料・議事録)に当たらないもの。
1769
1782
  // 元ツールのURLがまだ無い記録は、点にするが飛び先を付けない(#968 の書き換えを待つ)。
1770
1783
  // 名前は最後の段から拡張子を外したもの(`.md` のようなファイルの語を出さない)
1771
1784
  return addNode({ id: target, label: (0, evidence_names_1.pathFallbackLabel)(target), group: "silver" });
1772
1785
  }
1773
- return addNode({ id: target, label: target, group: "phantom" });
1786
+ unresolvedTargets++;
1787
+ return null;
1774
1788
  };
1775
1789
  const addRelationEdges = (fromId, relations) => {
1776
1790
  for (const r of relations) {
1777
1791
  // 🔴 `superseded_by` は描かない(#699 F3)。新しい側の `supersedes` の写しで、描くと同じ置き換えが 2 本になる
1778
1792
  if (exports.MIRROR_RELS.has(r.rel))
1779
1793
  continue;
1780
- edges.push({ from: fromId, to: classifyTarget(r.target), rel: r.rel });
1794
+ // 🔴 線にするのは記録どうしの根拠・生成元・関連・置換だけ(話題 about などは引かない)
1795
+ if (!exports.GRAPH_RELS.has(r.rel))
1796
+ continue;
1797
+ const to = classifyTarget(r.target);
1798
+ if (to)
1799
+ edges.push({ from: fromId, to, rel: r.rel });
1781
1800
  }
1782
1801
  };
1783
1802
  const addSourceEdge = (fromId, source) => {
1784
- if (source && source.trim() !== "")
1785
- edges.push({ from: fromId, to: classifyTarget(source.trim()), rel: "source" });
1803
+ if (source && source.trim() !== "") {
1804
+ const to = classifyTarget(source.trim());
1805
+ if (to)
1806
+ edges.push({ from: fromId, to, rel: "source" });
1807
+ }
1786
1808
  };
1787
1809
  const addReferenceEdges = (fromId, references) => {
1788
1810
  for (const ref of references) {
@@ -1791,21 +1813,16 @@ function buildGraph(decisions, home, sections, targetUrls, repoBaseUrl, book = (
1791
1813
  // (`[名前](URL)` の名前は `identifyEvidence` が書かれていた値から拾う。解決器が付けた名前は
1792
1814
  // 種類の既定のこともあるので、人の名前としては渡さない)
1793
1815
  const raw = REF_RAW.get(ref);
1794
- if (raw !== undefined)
1795
- edges.push({ from: fromId, to: classifyTarget(raw), rel: "reference" });
1796
- else
1797
- edges.push({ from: fromId, to: classifyTarget(ref.url ?? ref.label, ref.url && ref.label !== ref.url ? ref.label : undefined), rel: "reference" });
1798
- }
1799
- };
1800
- const addLinkEdges = (fromId, links) => {
1801
- for (const link of links) {
1802
- edges.push({ from: fromId, to: classifyTarget(link), rel: "link" });
1816
+ const to = raw !== undefined
1817
+ ? classifyTarget(raw)
1818
+ : classifyTarget(ref.url ?? ref.label, ref.url && ref.label !== ref.url ? ref.label : undefined);
1819
+ if (to)
1820
+ edges.push({ from: fromId, to, rel: "reference" });
1803
1821
  }
1804
1822
  };
1805
1823
  for (const d of decisions) {
1806
1824
  addRelationEdges(d.id, d.relations);
1807
1825
  addReferenceEdges(d.id, d.references);
1808
- addLinkEdges(d.id, d.bodyLinks);
1809
1826
  }
1810
1827
  // 決めた人の線(決定 → 関係者・#1094)。relations には書かれないので `decided_by[].member` から引く。
1811
1828
  // 🔴 相手の関係者がグラフに無ければ引かない(宛先の無い点を作らない=classifyTarget を通さない)
@@ -1818,16 +1835,12 @@ function buildGraph(decisions, home, sections, targetUrls, repoBaseUrl, book = (
1818
1835
  edges.push({ from: d.id, to: who.member, rel: "decided_by" });
1819
1836
  }
1820
1837
  }
1821
- if (home) {
1822
- addRelationEdges(home.id, home.relations);
1823
- addSourceEdge(home.id, home.source);
1824
- addLinkEdges(home.id, home.bodyLinks);
1825
- }
1826
1838
  for (const sec of sections) {
1827
1839
  for (const r of sec.records) {
1840
+ if (!nodes.has(r.id))
1841
+ continue; // 点にしない種類(Home・レポート・その他)の線は引かない
1828
1842
  addRelationEdges(r.id, r.relations);
1829
1843
  addSourceEdge(r.id, r.source);
1830
- addLinkEdges(r.id, r.bodyLinks);
1831
1844
  }
1832
1845
  }
1833
1846
  // 自己ループと重複辺を除去する(同じURLが複数回貼られていても辺は1本)
@@ -1841,6 +1854,9 @@ function buildGraph(decisions, home, sections, targetUrls, repoBaseUrl, book = (
1841
1854
  seen.add(key);
1842
1855
  return true;
1843
1856
  });
1857
+ if (unresolvedTargets > 0) {
1858
+ console.log(` つながり: 引けない根拠 ${unresolvedTargets} 件は点にしませんでした(記録でも元のツールの根拠でもない宛先)`);
1859
+ }
1844
1860
  return { nodes: [...nodes.values()], edges: deduped };
1845
1861
  }
1846
1862
  /** 反発(O(n²))。点が少ないうちはこれが最速。 */
@@ -2336,7 +2352,7 @@ async function buildSite(opts, views) {
2336
2352
  const rankedDecisions = (0, ranking_1.orderByRanking)(decisions, (d) => recordRelPaths.get(d.id), memoryRanking?.Decisions ?? null);
2337
2353
  // 期間フィルタ用の月(YYYY-MM)を日付から導出し、降順(新しい順)にする
2338
2354
  const months = uniqueSorted(decisions.map((d) => d.date.slice(0, 7)).filter((m) => /^\d{4}-\d{2}$/.test(m))).reverse();
2339
- const graph = buildGraph(decisions, home, graphSections, targetUrls, repo.baseUrl, evidenceBook);
2355
+ const graph = buildGraph(decisions, graphSections, targetUrls, repo.baseUrl, evidenceBook);
2340
2356
  computeLayout(graph); // 各ノードに x/y を書き込む(クライアントはこれを初期位置に使いウォームアップ省略)
2341
2357
  // 点の明るさと大きさ(engine #551)。順位のファイルが読めない案件では 1 点も付かない(今までと同じ画面)
2342
2358
  if (memoryRanking)
@@ -31,7 +31,7 @@ exports.memberSearchTerms = memberSearchTerms;
31
31
  /**
32
32
  * このリポジトリの内側の呼び名。外の ID と見なさない。
33
33
  *
34
- * 前半は記録の id の種類(`build.ts` の `GOLD_TYPES` と同じ)、後半は Silver の参照
34
+ * 前半は記録の id の種類(記録の種類の一覧と同じ)、後半は Silver の参照
35
35
  * (`build.ts` の `classifyTarget` が正本の URL に解決する 3 つ)。
36
36
  */
37
37
  const INSIDE_KINDS = new Set([
package/dist/render.js CHANGED
@@ -4714,8 +4714,8 @@ a:hover { text-decoration: underline; }
4714
4714
  別の切れ方をすると「設定したのと違う絵が出ている」と読まれる */
4715
4715
  .site-icon-slot { position: relative; flex: 0 0 auto; display: flex; align-items: center; }
4716
4716
  /* 大きさは案件名(18px・太字)の文字の高さに揃える(0.12.25 の 28px はオーナー実機で
4717
- 「小さくて見にくい」)。**丸は overflow: hidden にしない**——右下の「+」バッジを
4718
- 丸の外へ少しはみ出させるため。画像側(.site-icon-img)を丸く切る */
4717
+ 「小さくて見にくい」)。画像側(.site-icon-img)を丸く切る。
4718
+ 🔴 丸は見せるだけで押せない(仕様 common.screen-ops)ので、ホバーの効果・カーソル・メニューは持たない */
4719
4719
  .site-icon {
4720
4720
  position: relative;
4721
4721
  width: 36px; height: 36px; border-radius: 50%;
@@ -4729,59 +4729,7 @@ a:hover { text-decoration: underline; }
4729
4729
  display: flex; align-items: center; justify-content: center;
4730
4730
  width: 100%; height: 100%; border-radius: 50%;
4731
4731
  }
4732
- /* **「+」はホバー時だけ、右下に小さく重ねる**(常時は出さない)。
4733
- 受け口が無い案件(span.site-icon)には付かない(置けない口に誘いを出さない) */
4734
- .site-icon-plus {
4735
- position: absolute; right: -2px; bottom: -2px;
4736
- width: 16px; height: 16px; border-radius: 50%;
4737
- display: flex; align-items: center; justify-content: center;
4738
- background: var(--accent); color: #fff; font-size: 12px; font-weight: 700; line-height: 1;
4739
- box-shadow: 0 0 0 2px #fff;
4740
- opacity: 0; transition: opacity 0.12s ease;
4741
- }
4742
- button.site-icon:hover .site-icon-plus,
4743
- button.site-icon[aria-expanded="true"] .site-icon-plus { opacity: 1; }
4744
- /* 設定済みの丸は画像だけ。**押せることはホバーで出す**——常時ラベルを重ねると
4745
- 選んだロゴが読めなくなる(アイコンを設定した意味が薄れる) */
4746
- .site-icon-hover {
4747
- position: absolute; top: 0; left: 0; right: 0; bottom: 0; border-radius: 50%;
4748
- display: flex; align-items: center; justify-content: center;
4749
- background: rgba(0, 0, 0, 0.55); color: #fff;
4750
- font-size: 11px; font-weight: 600; line-height: 1;
4751
- opacity: 0; transition: opacity 0.12s ease;
4752
- }
4753
- button.site-icon:hover .site-icon-hover,
4754
- button.site-icon[aria-expanded="true"] .site-icon-hover { opacity: 1; }
4755
- /* **押せることはホバーで示す。** 受け口が無い案件の丸は span なので、この効果は付かない
4756
- ——押せない丸に指差しカーソルやリングを出すのが、画面の嘘のうちで一番たちが悪い */
4757
- button.site-icon { cursor: pointer; }
4758
- button.site-icon:hover, button.site-icon[aria-expanded="true"] { box-shadow: 0 0 0 2px var(--accent); }
4759
- /* **保存中(#378)は丸の上に .busy-overlay(共通部品)が載る。** その間はホバーの「変更」「+」
4760
- もリングも出さない(スピナーと重なって読めない・押せるように見せない)。
4761
- ホバー規則と同じ強さなので、**この行はホバー規則より後に置く** */
4762
- button.site-icon[aria-busy="true"] { cursor: wait; box-shadow: none; }
4763
- button.site-icon[aria-busy="true"] .site-icon-hover,
4764
- button.site-icon[aria-busy="true"] .site-icon-plus { opacity: 0; }
4765
4732
  .site-icon-img { width: 100%; height: 100%; border-radius: 50%; object-fit: cover; display: block; }
4766
- /* 丸を押したときだけ出るメニュー。スロットを基準に浮かせる(ヘッダーは sticky なので
4767
- ページを送っても位置がずれない) */
4768
- .site-icon-menu {
4769
- position: absolute; top: 44px; left: 0; z-index: 30; min-width: 220px;
4770
- background: #fff; border: 1px solid var(--border); border-radius: 10px;
4771
- box-shadow: 0 8px 24px rgba(0,0,0,.12); padding: 8px;
4772
- display: flex; flex-direction: column; gap: 6px;
4773
- }
4774
- .site-icon-menu[hidden] { display: none; }
4775
- .site-icon-menu button {
4776
- text-align: left; padding: 8px 10px; border-radius: 8px;
4777
- border: 1px solid var(--border); background: #fff; font-size: 13px; cursor: pointer;
4778
- }
4779
- .site-icon-menu button:hover { background: #f3f6fb; }
4780
- .site-icon-menu button:disabled { opacity: .6; cursor: default; }
4781
- /* 結果の行はメニューの中に置く(丸の横に出すとヘッダーの高さに押し込まれて読めない) */
4782
- .site-icon-status { font-size: 12.5px; padding: 2px 4px; }
4783
- .site-icon-status:empty { display: none; }
4784
- .site-icon-status-error { color: #b91c1c; }
4785
4733
  .record-count { color: var(--muted); font-size: 13px; }
4786
4734
  .ask-ai-btn {
4787
4735
  margin-left: auto; flex: 0 0 auto;
@@ -4842,14 +4790,6 @@ button.site-icon[aria-busy="true"] .site-icon-plus { opacity: 0; }
4842
4790
  box-shadow: var(--shadow); cursor: grab; display: block;
4843
4791
  }
4844
4792
  .graph-canvas.dragging { cursor: grabbing; }
4845
- .graph-legend {
4846
- display: flex; flex-wrap: wrap; gap: 12px; align-items: center;
4847
- margin: 10px 2px 0; font-size: 12.5px; color: var(--muted);
4848
- }
4849
- .graph-legend .dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; margin-right: 4px; vertical-align: -1px; }
4850
- .graph-legend .line { display: inline-block; width: 16px; height: 0; border-top: 3px solid; margin-right: 4px; vertical-align: 4px; }
4851
- .graph-legend .sep { color: var(--muted); margin-left: 8px; padding-left: 8px; border-left: 1px solid var(--border); }
4852
- .graph-legend label { display: inline-flex; align-items: center; gap: 4px; cursor: pointer; color: var(--text); }
4853
4793
 
4854
4794
  .graph-reset {
4855
4795
  position: absolute; top: 12px; right: 12px; z-index: 2;
@@ -8234,10 +8174,10 @@ ${story_1.splitByNameLinks.toString()}
8234
8174
  btnUrl: base ? base + "/actions/workflows/update-gold.yml" : null,
8235
8175
  },
8236
8176
  "graph": {
8237
- text: "決定・用語・レポートと、その根拠になった課題・議事録・資料のつながりを自動でつないだ地図です。点をクリックすると、そのページや実際のシステム(Backlog・GitHub等)に移動できます。",
8177
+ text: "決定・ルール・用語・関係者と、その根拠(元のツール)のつながりの地図です。点をクリックすると、そのページや元のツール(Backlog・GitHub等)に移動できます。",
8238
8178
  },
8239
8179
  "explorer": {
8240
- text: "決定・用語・レポートを起点に、つながり(根拠・出典・リンク)を1列ずつ辿る表示です。行をクリックすると右に次の列が開きます。",
8180
+ text: "決定・ルール・用語・関係者を起点に、つながり(根拠・出典)を1列ずつ辿る表示です。行をクリックすると右に次の列が開きます。",
8241
8181
  },
8242
8182
  "story": {
8243
8183
  text: "このプロジェクトの日々の動きを、日次・週次・月次に分けてまとめた記録です。直したいことや足したいことは、各カードの「GitHubで編集」か「AIで編集」から書けます。",
@@ -8479,7 +8419,7 @@ ${story_1.splitByNameLinks.toString()}
8479
8419
 
8480
8420
  var sec = el("section", { class: "tut-section" });
8481
8421
  sec.appendChild(el("h2", { class: "tut-section-head", text: "はじめに — このプロジェクトを掴む" + challenges.length + "ステップ" }));
8482
- sec.appendChild(el("p", { class: "tut-intro", text: "AIS Viewer と Claude Code を使ってこのプロジェクトを掴むための" + challenges.length + "ステップです。各ボタンを押すと、質問が入力済みの状態で Claude Code Web が開きます(このリポジトリへのアクセス権が必要です)。" }));
8422
+ sec.appendChild(el("p", { class: "tut-intro", text: "ビューアと Claude Code を使ってこのプロジェクトを掴むための" + challenges.length + "ステップです。各ボタンを押すと、質問が入力済みの状態で Claude Code Web が開きます(このリポジトリへのアクセス権が必要です)。" }));
8483
8423
 
8484
8424
  // 進捗ヘッダ({完了数}/5・バー・全完了で祝福)
8485
8425
  var doneCount = challenges.filter(function (c) { return tutIsDone(c.n); }).length;
@@ -8518,121 +8458,10 @@ ${story_1.splitByNameLinks.toString()}
8518
8458
 
8519
8459
  // ---------- 案件アイコン(#348) ----------
8520
8460
  //
8521
- // ポータルの案件一覧はカードにアイコンを出す。置き場はこの案件のリポジトリの
8522
- // Cortex/icon.<ext> で、**設定する口はこのビューアにしかない**——ポータル側に
8523
- // 置くと「誰の案件か」の判定を先に作らねばならず、案件を知っている人が
8524
- // その場で直せる形にならない。
8525
- //
8526
- // **出す場所はヘッダーの案件名の左**(0.12.24 の Home 本文から移した・#348)。
8527
- // ヘッダーは全タブ共通なので、どのタブを開いていても同じ場所に居る。
8528
- // 押せる口はメニューの中に畳む——常時見えるボタンは、たまにしか使わない操作には重い。
8529
- //
8530
- // 上限はサーバ側(logic.mjs の PROJECT_ICON_MAX_BYTES)と同じ値。画面の門は
8531
- // 「送る前に断る」ためのもので、正本はサーバ側
8532
- var PROJECT_ICON_MAX_BYTES = 1024 * 1024;
8533
-
8534
- /* ---- 反映待ち(pending)----------------------------------------------------
8535
- * 静的サイトなので、保存の commit が入っても**次のビルドまで SiteData は古いまま**。
8536
- * 何もしないと「入れたのにまだ頭文字のまま」「外したのにまだ絵が出る」と見える。
8537
- * 0.12.26 まではその待ち時間を「数分後に反映されます」の文で説明していたが、
8538
- * オーナー実機で却下された(待たされている事実は説明されても消えない)。
8539
- *
8540
- * そこで**削除のトゥームストーンと同じ型**で、保存できたことをこのブラウザに覚える。
8541
- * 覚えるのは押した本人のブラウザだけで、他の端末・他人には次のビルドで届く。
8542
- *
8543
- * ⚠ **キーは案件キー**(リポジトリのスラグではない)。同じオリジンに配られる
8544
- * ポータルの一覧カードが、同じキーでこの仮画像を読んで先に出すため。
8545
- *
8546
- * ⚠ **いつまでも仮画像を出し続けない。** 本物が焼き直された後も残していると、
8547
- * 別の画像に差し替えたつもりの人に、いつまでも古い仮画像が見え続ける
8548
- * (破棄の条件は iconPendingStale=トゥームストーンと同じ判定)。
8549
- *
8550
- * ⚠ 反映待ちの入れ物に p という変数名を使わないこと。fleet-passthrough の検査は
8551
- * その名前のドット参照を「艦隊データの欄」として走査するので、
8552
- * ここで使うと build.ts の受け渡し漏れに化けて赤くなる
8553
- */
8554
- function iconPendingKey() {
8555
- return SITE.intake ? "cortex-icon-pending:" + SITE.intake.projectKey : "";
8556
- }
8557
- /** 覚えている反映待ち(無い・壊れている・形が違うなら null) */
8558
- function readIconPending() {
8559
- var key = iconPendingKey();
8560
- if (!key) return null;
8561
- try {
8562
- var raw = localStorage.getItem(key);
8563
- if (!raw) return null;
8564
- var pend = JSON.parse(raw);
8565
- if (!pend || typeof pend !== "object") return null;
8566
- // 形の版と案件キーは**ポータルの読み側(usePendingProjectIcon)と同じ検査**にする。
8567
- // 将来 v:2 を出したとき、古い画面が新しい形を読み違えないため
8568
- if (pend.v !== 1 || pend.projectKey !== SITE.intake.projectKey) return null;
8569
- if (pend.kind === "remove") return pend;
8570
- // set は仮画像そのものが要る。**data:image/ 以外は載せない**(外部URLを持ち込まない・#110。
8571
- // ポータル側と同じ検査)
8572
- if (pend.kind === "set" && typeof pend.dataUrl === "string" && pend.dataUrl.indexOf("data:image/") === 0) return pend;
8573
- return null;
8574
- } catch (e) { return null; }
8575
- }
8576
- function writeIconPending(entry) {
8577
- var key = iconPendingKey();
8578
- if (!key) return;
8579
- // 1MB の画像を data: にすると 1.4MB 前後。localStorage の枠を超えることがあるので、
8580
- // 失敗しても操作は成功したまま進める(次のビルドで本物が出る)
8581
- try { localStorage.setItem(key, JSON.stringify(entry)); } catch (e) {}
8582
- }
8583
- function clearIconPending() {
8584
- var key = iconPendingKey();
8585
- if (!key) return;
8586
- try { localStorage.removeItem(key); } catch (e) {}
8587
- }
8588
- /**
8589
- * この反映待ちはもう用済みか(=保存より後のビルドが来ている)。
8590
- * **判定はトゥームストーン(tombstoneStale)と同じ**——同じ性質の待ちなので、
8591
- * 破棄の条件を2つ持つと、片方だけ直したときに挙動が割れる。
8592
- */
8593
- function iconPendingStale(pend, nowMs) {
8594
- if (!pend) return false;
8595
- return pendingStale(pend.at, pend.gen0, nowMs);
8596
- }
8597
- /** いま覚えている反映待ちを記録する(保存・解除に共通の形) */
8598
- function rememberIconPending(kind, dataUrl) {
8599
- var entry = { v: 1, projectKey: SITE.intake ? SITE.intake.projectKey : "", kind: kind, at: Date.now(), gen0: SITE.generatedAt || "" };
8600
- if (dataUrl) entry.dataUrl = dataUrl;
8601
- writeIconPending(entry);
8602
- }
8603
- /** 選んだ画像を data: にする。読めなければ空文字(仮画像は出さず、本物のビルドを待つ) */
8604
- function fileToDataUrl(file) {
8605
- return new Promise(function (resolve) {
8606
- try {
8607
- var r = new FileReader();
8608
- r.onload = function () { resolve(typeof r.result === "string" ? r.result : ""); };
8609
- r.onerror = function () { resolve(""); };
8610
- r.readAsDataURL(file);
8611
- } catch (e) { resolve(""); }
8612
- });
8613
- }
8614
-
8615
- /** いま画面に出すべきアイコン(無ければ null)。**反映待ちがビルド時の値に優先する** */
8616
- function projectIconCurrent() {
8617
- var pend = readIconPending();
8618
- if (pend) return pend.kind === "remove" ? null : { file: "", src: pend.dataUrl };
8619
- var q = SITE.projectIcon;
8620
- // ビルドが同梱した画像はサイト内の相対パス。**外部URLは載せない**
8621
- return q ? { file: q.file, src: q.file } : null;
8622
- }
8623
-
8624
- /**
8625
- * 名前から拡張子を引く(jpeg は jpg へ寄せる)。許可外・拡張子なしは null。
8626
- * **サーバ側(logic.mjs の projectIconExtOf)と同じ判断**を画面でも先にやるのは、
8627
- * 通らないと分かっているファイルを1MB 送ってから断らないため
8628
- */
8629
- function projectIconExtOfName(name) {
8630
- var m = /\\.([A-Za-z0-9]+)$/.exec(String(name || ""));
8631
- if (!m) return null;
8632
- var e = m[1].toLowerCase();
8633
- if (e === "jpeg") e = "jpg";
8634
- return e === "png" || e === "jpg" || e === "webp" ? e : null;
8635
- }
8461
+ // ポータルの案件一覧はカードにアイコンを出す。置き場はこの案件のリポジトリの Cortex/icon.<ext> で、
8462
+ // ビルドが同梱した画像(SITE.projectIcon)を**ヘッダーの案件名の左に見せるだけ**にする。
8463
+ // 🔴 **画面からアイコンを置く・外す操作は持たない**(仕様 common.screen-ops「案件のアイコンの操作は持たない」・cortex-engine #1781)。
8464
+ // 変えたいときはリポジトリの Cortex/icon.<ext> を GitHub で直す
8636
8465
 
8637
8466
  // いま出ているトーストを消すタイマ。**1本だけ持つ**——連続で出したときに
8638
8467
  // 古いタイマが残っていると、2つ目が出た 0.2 秒後に消える
@@ -8676,35 +8505,6 @@ ${story_1.splitByNameLinks.toString()}
8676
8505
  });
8677
8506
  }
8678
8507
 
8679
- // ヘッダーの丸のメニューが開いているか。**DOM ではなくこの変数が正本。**
8680
- // 保存・解除のあとに丸を描き直すので、開閉を DOM だけに持たせると描き直しで畳まれ、
8681
- // 「✓ 保存しました」と保存先のリンクが一度も見えないまま消える
8682
- var siteIconMenuOpen = false;
8683
- // document に付ける開閉のリスナーは1組だけ。描き直しのたびに足すと Escape 1回で
8684
- // 同じ処理が何度も走る(見た目は同じなので気づけない)
8685
- var siteIconGlobalBound = false;
8686
- // 保存中か(画像を選ぶ/変える/外す の3段が走っている間)。**DOM ではなくこの変数が正本**
8687
- // (#378)。描き直しで丸が作り直されても、renderSiteIcon がこれを見てオーバーレイを付け直す。
8688
- // setProjectIcon / removeProjectIcon の入口で見て、走っている間の2回目を捨てる(二重送信防止)
8689
- var siteIconSaving = false;
8690
- // いま丸に載っているオーバーレイの外し方(無ければ null)
8691
- var siteIconBusyEnd = null;
8692
-
8693
- /**
8694
- * 丸の保存中の見た目を付ける/外す。丸は disabled にして押せなくする
8695
- * (メニューの開閉もここで止まる。丸のクリックは siteIconSaving でも見る)
8696
- */
8697
- function setSiteIconBusy(circle, on) {
8698
- if (siteIconBusyEnd) { siteIconBusyEnd(); siteIconBusyEnd = null; }
8699
- if (!circle) return;
8700
- if (on) {
8701
- siteIconBusyEnd = beginOverlaySpinner(circle, "保存中…");
8702
- circle.disabled = true;
8703
- } else {
8704
- circle.disabled = false;
8705
- }
8706
- }
8707
-
8708
8508
  /**
8709
8509
  * 案件名の頭文字1字。**ポータルの ProjectIcon.tsx(initialOf)と同じ取り方**にする
8710
8510
  * ——同じ案件がポータルとビューアで違う字になると「別の案件を見ている」と読まれる。
@@ -8715,236 +8515,36 @@ ${story_1.splitByNameLinks.toString()}
8715
8515
  return Array.from(String(SITE.title || "").trim())[0] || "?";
8716
8516
  }
8717
8517
 
8718
- /** 丸の中の頭文字(薄くするかどうかは CSS が丸の種類で決める) */
8518
+ /** 丸の中の頭文字 */
8719
8519
  function siteIconInitialEl() {
8720
8520
  return el("span", { class: "site-icon-initial", text: siteInitial() });
8721
8521
  }
8722
8522
 
8723
- /** メニューの開閉。描き直さずに hidden と aria-expanded だけ動かす */
8724
- function setSiteIconMenu(open) {
8725
- siteIconMenuOpen = !!open;
8726
- var slot = document.getElementById("site-icon-slot");
8727
- if (!slot) return;
8728
- var menu = slot.querySelector(".site-icon-menu");
8729
- if (menu) {
8730
- if (siteIconMenuOpen) menu.removeAttribute("hidden");
8731
- else menu.setAttribute("hidden", "");
8732
- }
8733
- var circle = slot.querySelector("button.site-icon");
8734
- if (circle) circle.setAttribute("aria-expanded", siteIconMenuOpen ? "true" : "false");
8735
- }
8736
-
8737
8523
  /**
8738
8524
  * ヘッダーの案件名の左の丸を描く(#site-icon-slot の中身を作り直す)。
8739
8525
  *
8740
8526
  * **丸は常に出す**(未設定なら頭文字)——ポータルの一覧と同じ顔をここでも見せる。
8741
- * **押せる形にするのは受け口(intake)がある案件だけ。** 無ければ span にして
8742
- * メニューもファイル選択も付けない——押して何も起きない口は「壊れている」と読まれる。
8743
- *
8527
+ * 🔴 **押せる形にしない**(受け口がある案件でも span・メニューもファイル選択も付けない)。仕様 common.screen-ops
8744
8528
  */
8745
8529
  function renderSiteIcon() {
8746
8530
  var slot = document.getElementById("site-icon-slot");
8747
8531
  if (!slot) return;
8748
- var intake = SITE.intake;
8749
- // **本物が追いついたら仮画像を捨てる。** 残したままだと、次に差し替えた人にも
8750
- // 古い仮画像が見え続ける(削除のトゥームストーンと同じ捨て方)
8751
- var pend = readIconPending();
8752
- if (pend && iconPendingStale(pend, pendingSiteTimeMs(SITE.generatedAt))) clearIconPending();
8753
- var cur = projectIconCurrent();
8532
+ var q = SITE.projectIcon;
8754
8533
  slot.textContent = "";
8755
-
8756
- var circle = el(intake ? "button" : "span", { class: "site-icon" });
8757
- if (intake) {
8758
- circle.setAttribute("type", "button");
8759
- circle.setAttribute("aria-haspopup", "menu");
8760
- circle.setAttribute("aria-expanded", siteIconMenuOpen ? "true" : "false");
8761
- // 未設定は「+」が見えているので、説明は「変更」ではなく置ける話にしない——
8762
- // 読み上げと tooltip は、いまの状態でできることをそのまま言う
8763
- var label = cur ? "プロジェクトアイコンを変更/外す" : "プロジェクトアイコンを設定";
8764
- circle.setAttribute("title", label);
8765
- circle.setAttribute("aria-label", label);
8766
- } else {
8767
- // すぐ隣に案件名が文字で出ているので、読み上げで名前を2回言わせない
8768
- circle.setAttribute("aria-hidden", "true");
8769
- }
8770
- if (cur) {
8771
- // **設定済みは画像だけ。** 頭文字も「+」も重ねない(選んだロゴが読めなくなる)
8772
- var img = el("img", { class: "site-icon-img", src: cur.src, alt: "" });
8773
- // **onerror 属性で書かない(CSP でインラインハンドラは動かない)。**
8774
- // 仮画像の data: が配信先の img-src で拒まれることがあるので、そのときは頭文字に倒す
8534
+ // すぐ隣に案件名が文字で出ているので、読み上げで名前を2回言わせない
8535
+ var circle = el("span", { class: "site-icon", "aria-hidden": "true" });
8536
+ if (q && q.file) {
8537
+ // **設定済みは画像だけ**(頭文字を重ねない)。ビルドが同梱した画像はサイト内の相対パスで、外部URLは載せない
8538
+ var img = el("img", { class: "site-icon-img", src: q.file, alt: "" });
8539
+ // **onerror 属性で書かない(CSP でインラインハンドラは動かない)。** 読めなければ頭文字に倒す
8775
8540
  img.addEventListener("error", function () {
8776
8541
  if (img.parentNode) img.parentNode.replaceChild(siteIconInitialEl(), img);
8777
8542
  });
8778
8543
  circle.appendChild(img);
8779
- // 押せる案件だけ、ホバーで「変更」と出す(見た目は CSS の opacity で切り替える)
8780
- if (intake) circle.appendChild(el("span", { class: "site-icon-hover", "aria-hidden": "true", text: "変更" }));
8781
8544
  } else {
8782
8545
  circle.appendChild(siteIconInitialEl());
8783
- // **未設定の押せる丸には、ホバー時だけ右下に小さく「+」を出す**(CSS の opacity)。
8784
- // 頭文字ははっきり出す——0.12.25 の「薄い頭文字+大きな+」は「+だけの丸」に見えた
8785
- if (intake) circle.appendChild(el("span", { class: "site-icon-plus", "aria-hidden": "true", text: "+" }));
8786
8546
  }
8787
8547
  slot.appendChild(circle);
8788
- if (!intake) return;
8789
- // 保存中に描き直されても、オーバーレイと押せない状態を引き継ぐ(正本は siteIconSaving)。
8790
- // いまの呼び出し経路(起動時1回・ui.busy(false) の後の redraw)では保存中に来ないので
8791
- // 防御。将来タブ切り替え等でヘッダーを描き直すようになっても、ぐるぐるが消えない
8792
- if (siteIconSaving) setSiteIconBusy(circle, true);
8793
-
8794
- circle.addEventListener("click", function () {
8795
- // 保存中は丸が disabled なので click 自体が飛ばない(ブラウザの既定)。これは防御
8796
- if (siteIconSaving) return;
8797
- setSiteIconMenu(!siteIconMenuOpen);
8798
- });
8799
-
8800
- var menu = el("div", { class: "site-icon-menu", role: "menu" });
8801
- if (!siteIconMenuOpen) menu.setAttribute("hidden", "");
8802
-
8803
- var ui = {
8804
- // 描き直すと押していたボタンが DOM から消えてフォーカスが body に落ちるので、
8805
- // 丸へ戻す(キーボードの人が位置を失わない。Escape は document 側なので効く)
8806
- redraw: function () {
8807
- renderSiteIcon();
8808
- ui.refocus();
8809
- },
8810
- // 丸へフォーカスを戻す。失敗のあとも呼ぶ——押していた項目は disabled にした瞬間に
8811
- // フォーカスを失って body に落ちるので、戻さないとキーボードの人がやり直す位置を失う
8812
- refocus: function () {
8813
- var s = document.getElementById("site-icon-slot");
8814
- var c = s && s.querySelector("button.site-icon");
8815
- if (c && typeof c.focus === "function") c.focus();
8816
- },
8817
- input: null,
8818
- buttons: [],
8819
- };
8820
- // 送信中は全部を止める(#378: 丸にオーバーレイ+スピナー・丸とメニューを押せなくする)。
8821
- // **元から押せない項目は押せないまま戻す**——
8822
- // 未設定の「画像を外す」が、失敗して戻ったときだけ押せるようになるのを防ぐ
8823
- ui.busy = function (flag) {
8824
- siteIconSaving = !!flag;
8825
- ui.buttons.forEach(function (b) { b.disabled = flag ? true : !!b.lockedOff; });
8826
- var s = document.getElementById("site-icon-slot");
8827
- setSiteIconBusy(s && s.querySelector("button.site-icon"), siteIconSaving);
8828
- };
8829
-
8830
- var input = el("input", { type: "file", accept: "image/png,image/jpeg,image/webp", hidden: "" });
8831
- ui.input = input;
8832
- input.addEventListener("change", function () {
8833
- var f = input.files && input.files[0];
8834
- // **読んだらすぐ value を戻す**(成功時だけ戻す形だと、形式で断られた直後に
8835
- // 同じファイルを選び直しても change が飛ばず、直す経路がここで死ぬ)
8836
- try { input.value = ""; } catch (e) {}
8837
- if (f) setProjectIcon(f, ui);
8838
- });
8839
- // **項目は常にこの2つ・この順。** 状態で数が変わると、押す場所が毎回動く
8840
- // (未設定のときは「外す」を押せなくするだけで、消しはしない)
8841
- var choose = el("button", { class: "site-icon-choose", type: "button", role: "menuitem", text: cur ? "画像を変える" : "画像を選ぶ" });
8842
- choose.addEventListener("click", function () { input.click(); });
8843
- ui.buttons.push(choose);
8844
- menu.appendChild(choose);
8845
- var remove = el("button", { class: "site-icon-remove", type: "button", role: "menuitem", text: "画像を外す" });
8846
- if (!cur) {
8847
- remove.disabled = true;
8848
- remove.lockedOff = true;
8849
- }
8850
- remove.addEventListener("click", function () { removeProjectIcon(ui); });
8851
- ui.buttons.push(remove);
8852
- menu.appendChild(remove);
8853
- menu.appendChild(input);
8854
- slot.appendChild(menu);
8855
-
8856
- if (!siteIconGlobalBound) {
8857
- siteIconGlobalBound = true;
8858
- // 外を押したら閉じる。**スロットの中では閉じない**——「画像を選ぶ」を押した
8859
- // 瞬間に畳まれると、選び終えたときに結果を書く場所が無い
8860
- document.addEventListener("click", function (ev) {
8861
- if (!siteIconMenuOpen) return;
8862
- var s = document.getElementById("site-icon-slot");
8863
- if (s && s.contains(ev.target)) return;
8864
- setSiteIconMenu(false);
8865
- });
8866
- document.addEventListener("keydown", function (ev) {
8867
- if (siteIconMenuOpen && (ev.key === "Escape" || ev.key === "Esc")) setSiteIconMenu(false);
8868
- });
8869
- }
8870
- }
8871
-
8872
- /**
8873
- * 画像を1枚選んで保存する。
8874
- *
8875
- * **画面側でも門を張る。** サーバ側(planProjectIconSet)が正本だが、ここで断れる
8876
- * ものを送ると、1MB を上げきってから「大きすぎます」と言うことになる
8877
- */
8878
- async function setProjectIcon(file, ui) {
8879
- // 走っている間の2回目は捨てる(丸もメニューも止めてあるが、正本はこの変数)
8880
- if (siteIconSaving) return;
8881
- function fail(msg) { showToast("✗ " + msg, true); ui.busy(false); ui.refocus(); }
8882
- // 画面側の門。**サーバ側と同じ文言**で、送る前に断る(fail で「✗ 」を揃える)
8883
- if (!projectIconExtOfName(file.name)) return fail("PNG・JPEG・WebP の画像を選んでください");
8884
- if (file.size > PROJECT_ICON_MAX_BYTES) return fail("1MB までの画像を選んでください");
8885
- if (file.size === 0) return fail("画像が空です(0 バイト)。別の画像を選んでください");
8886
-
8887
- var intake = SITE.intake;
8888
- var ctype = file.type || "application/octet-stream";
8889
- // 押した瞬間からぐるぐる(#378)。成功なら redraw で新しい丸に、失敗なら fail で元の丸に戻る
8890
- ui.busy(true);
8891
-
8892
- var pres;
8893
- try {
8894
- pres = await intakeJson(callIntake("presign", {
8895
- projectKey: intake.projectKey, filename: file.name, contentType: ctype, purpose: "project-icon",
8896
- }));
8897
- } catch (net) { return fail("サーバーに接続できませんでした。通信環境を確認して、もう一度お試しください。"); }
8898
- if (!pres.ok || !pres.j.url) return fail((pres.j && pres.j.message) || "アップロードURLの取得に失敗しました");
8899
-
8900
- // 署名付きURLへの PUT だけは intake ではないので素の fetch(submitIntake と同じ)
8901
- var put;
8902
- try {
8903
- put = await fetch(pres.j.url, { method: "PUT", headers: { "content-type": ctype }, body: file });
8904
- } catch (net2) { return fail("アップロードに失敗しました(もう一度お試しください)"); }
8905
- if (!put.ok) return fail("アップロードに失敗しました(" + put.status + ")");
8906
-
8907
- var saved;
8908
- try {
8909
- saved = await intakeJson(callIntake("project-icon-set", {
8910
- projectKey: intake.projectKey, key: pres.j.key, filename: file.name,
8911
- }));
8912
- } catch (net3) { return fail("サーバーに接続できませんでした。通信環境を確認して、もう一度お試しください。"); }
8913
- if (!saved.ok) return fail((saved.j && saved.j.message) || "保存できませんでした");
8914
-
8915
- // **選んだ画像そのものを反映待ちに置く。** blob: にしないのは、
8916
- // ポータルの一覧カード(別のページ)が同じ画像を読むため——blob: は
8917
- // 作ったページの中でしか開けない
8918
- var dataUrl = await fileToDataUrl(file);
8919
- // **読めなかったら反映待ちを書かない**(サーバ側の保存は済んでいるので、仮画像だけ
8920
- // 諦めて次のビルドを待つ)。空の set を書くと、直前の有効な反映待ちを上書きして消す
8921
- if (dataUrl) rememberIconPending("set", dataUrl);
8922
- // **同じファイルをもう一度選べるようにする。** value を戻さないと、選び直しても
8923
- // change が飛ばない(形式で断られた直後に選び直す経路がここで死ぬ)
8924
- if (ui.input) ui.input.value = "";
8925
- ui.busy(false);
8926
- // 丸がその場で変わるので、メニューは畳んでよい(結果はトーストが伝える)
8927
- setSiteIconMenu(false);
8928
- ui.redraw();
8929
- showToast("保存しました", false);
8930
- }
8931
-
8932
- /** 画像を外す。**確認は出さない**——押し間違えても、もう一度選び直せば戻る操作 */
8933
- async function removeProjectIcon(ui) {
8934
- // 保存中はメニューの項目が disabled なので普通は来ない(setProjectIcon と同じ防御)
8935
- if (siteIconSaving) return;
8936
- function fail(msg) { showToast("✗ " + msg, true); ui.busy(false); ui.refocus(); }
8937
- ui.busy(true);
8938
- var res;
8939
- try {
8940
- res = await intakeJson(callIntake("project-icon-remove", { projectKey: SITE.intake.projectKey }));
8941
- } catch (net) { return fail("サーバーに接続できませんでした。通信環境を確認して、もう一度お試しください。"); }
8942
- if (!res.ok) return fail((res.j && res.j.message) || "外せませんでした");
8943
- rememberIconPending("remove", "");
8944
- ui.busy(false);
8945
- setSiteIconMenu(false);
8946
- ui.redraw();
8947
- showToast("外しました", false);
8948
8548
  }
8949
8549
 
8950
8550
  // ---------- ホーム(着地画面: Markdownをそのまま表示) ----------
@@ -20332,42 +19932,30 @@ ${story_1.splitByNameLinks.toString()}
20332
19932
  term: { color: "#4ade80", label: "用語集" },
20333
19933
  member: { color: "#f472b6", label: "関係者" },
20334
19934
  rule: { color: "#fde047", label: "ルール" },
20335
- report: { color: "#22d3ee", label: "レポート" },
20336
- overview: { color: "#fb923c", label: "Home" },
20337
- record: { color: "#0e7490", label: "その他レコード" },
20338
19935
  silver: { color: "#9ca3af", label: "根拠(課題・議事録・資料・リンク等)" },
20339
19936
  bronze: { color: "#9ca3af", label: "根拠(課題・議事録・資料・リンク等)" },
20340
19937
  external: { color: "#9ca3af", label: "根拠(課題・議事録・資料・リンク等)" },
20341
- phantom: { color: "#d4d9e0", label: "参照先が見つからないもの" },
20342
19938
  };
19939
+ /* 点の色は区画を見分けるためだけに残す。凡例(分類の一覧)は出さない(仕様 graph.content-only) */
19940
+ var GROUP_FALLBACK = { color: "#9ca3af" };
20343
19941
  /* 線(関係)の種類ごとの色・ラベルと、「Goldどうし」か「根拠」かの分類(engine #551・分類A 10/1)。
20344
19942
  * **一覧(凡例)はこの表から組む**ので、将来 about(決定・ルール→用語)や is_a/part_of、
20345
19943
  * 人→用語・人→人の線を足すときは、ここに1行足すだけでよい——凡例もトグルも自動で追従し、
20346
19944
  * 既存の行・色は変わらない(語彙が増えても凡例が崩れない作り)。
20347
19945
  * kind: "gold"=Goldのレコードどうしを結ぶ線/"evidence"=Goldがその根拠を指す線(弱いつながり)。 */
20348
19946
  var REL_META = {
20349
- based_on: { color: "#2563eb", label: "根拠", kind: "gold" },
20350
- derived_from: { color: "#0d9488", label: "生成元", kind: "gold" },
20351
- relates_to: { color: "#94a3b8", label: "関連", kind: "gold" },
20352
- supersedes: { color: "#dc2626", label: "置換", kind: "gold" },
20353
- reference: { color: "#d1d5db", label: "参照", kind: "evidence" },
20354
- source: { color: "#d1d5db", label: "出典", kind: "evidence" },
20355
- link: { color: "#e2e8f0", label: "リンク", kind: "evidence" },
20356
- // 話題の線(決定・ルール・関係者 → 用語・engine #551 の about)。名前の一致で機械が付けた手がかりなので、
20357
- // 根拠の線と同じく細く薄い「ゆるいつながり」として描き、凡例は日本語の 1 行(栓 about_relations の on の条件)
20358
- about: { color: "#ddd6fe", label: "話題", kind: "topic" },
20359
- // 決めた人の線(決定 → 関係者・#1094)。relations ではなく decided_by から引く。
20360
- // 🔴 凡例には**この線がグラフにあるときだけ**出す(欄の無い案件の凡例を変えない)
20361
- decided_by: { color: "#f472b6", label: "決めた人", kind: "gold", onlyWhenPresent: true },
19947
+ based_on: { color: "#2563eb", label: "根拠" },
19948
+ derived_from: { color: "#0d9488", label: "生成元" },
19949
+ relates_to: { color: "#94a3b8", label: "関連" },
19950
+ supersedes: { color: "#dc2626", label: "置換" },
19951
+ reference: { color: "#d1d5db", label: "参照" },
19952
+ source: { color: "#d1d5db", label: "出典" },
19953
+ decided_by: { color: "#f472b6", label: "決めた人" },
20362
19954
  };
20363
- var WEAK_RELS = {};
20364
19955
  var REL_COLOR = {};
20365
- Object.keys(REL_META).forEach(function (r) {
20366
- REL_COLOR[r] = REL_META[r].color;
20367
- if (REL_META[r].kind === "evidence" || REL_META[r].kind === "topic") WEAK_RELS[r] = true;
20368
- });
19956
+ Object.keys(REL_META).forEach(function (r) { REL_COLOR[r] = REL_META[r].color; });
20369
19957
  // period: 期間で光らせる(engine #551)。unit は all(光らせない)/day/week、day は基準の日(YYYY-MM-DD)
20370
- var graphState = { showWeak: true, q: "", period: { unit: "all", day: null } };
19958
+ var graphState = { q: "", period: { unit: "all", day: null } };
20371
19959
 
20372
19960
  /** 期間の範囲(両端を含む YYYY-MM-DD)。「すべて」・日付が無いときは null。週は月曜〜日曜(ストーリーの週と同じ区切り) */
20373
19961
  function graphPeriodRange(period) {
@@ -20399,7 +19987,7 @@ ${story_1.splitByNameLinks.toString()}
20399
19987
  countEl.textContent = "点 " + graph.nodes.length + " / つながり " + graph.edges.length;
20400
19988
 
20401
19989
  var filters = el("div", { class: "filters" });
20402
- var search = el("input", { type: "search", placeholder: "キーワードで探す(決定・用語・レポートは本文の中まで探します。見つかった点とそのつながりが光ります)" });
19990
+ var search = el("input", { type: "search", placeholder: "キーワードで探す(決定・ルール・用語・関係者は本文の中まで探します。見つかった点とそのつながりが光ります)" });
20403
19991
  search.value = graphState.q;
20404
19992
  filters.appendChild(search);
20405
19993
  app.appendChild(filters);
@@ -20411,43 +19999,7 @@ ${story_1.splitByNameLinks.toString()}
20411
19999
  wrap.appendChild(canvas);
20412
20000
  app.appendChild(wrap);
20413
20001
 
20414
- // 凡例 + 弱い辺のトグル
20415
- var legend = el("div", { class: "graph-legend" });
20416
- var legendShown = {};
20417
- Object.keys(GROUP_STYLE).forEach(function (g) {
20418
- var anyNode = graph.nodes.some(function (n) { return n.group === g; });
20419
- if (!anyNode || legendShown[GROUP_STYLE[g].label]) return;
20420
- legendShown[GROUP_STYLE[g].label] = true;
20421
- var item = el("span", {}, [el("span", { class: "dot" }), document.createTextNode(GROUP_STYLE[g].label)]);
20422
- item.querySelector(".dot").style.background = GROUP_STYLE[g].color;
20423
- legend.appendChild(item);
20424
- });
20425
-
20426
- // 線(関係)の色凡例。グラフ内の有無に関わらず全種別を、Goldどうし/根拠の2群で表示する
20427
- [["gold", "線(Goldどうし):"], ["evidence", "線(根拠):"], ["topic", "線(話題):"]].forEach(function (pair) {
20428
- var kind = pair[0], heading = pair[1];
20429
- var keys = Object.keys(REL_META).filter(function (r) {
20430
- if (REL_META[r].kind !== kind) return false;
20431
- return !REL_META[r].onlyWhenPresent || graph.edges.some(function (e) { return e.rel === r; });
20432
- });
20433
- if (!keys.length) return;
20434
- legend.appendChild(el("span", { class: "sep", text: heading }));
20435
- keys.forEach(function (r) {
20436
- var li = el("span", {}, [el("span", { class: "line" }), document.createTextNode(REL_META[r].label)]);
20437
- li.querySelector(".line").style.borderTopColor = REL_META[r].color;
20438
- legend.appendChild(li);
20439
- });
20440
- });
20441
-
20442
- var toggle = el("label", {});
20443
- var cb = el("input", { type: "checkbox" });
20444
- cb.checked = graphState.showWeak;
20445
- cb.addEventListener("change", function () { graphState.showWeak = cb.checked; renderList(); });
20446
- toggle.appendChild(cb);
20447
- toggle.appendChild(document.createTextNode("ゆるいつながり(参照・本文リンク・話題)も表示"));
20448
- legend.appendChild(toggle);
20449
- app.appendChild(legend);
20450
-
20002
+ // 🔴 凡例(分類・線の種類の一覧)と、根拠の点を隠す切り替えは持たない(仕様 graph.content-only / graph.evidence-always)
20451
20003
  startGraph(canvas, search, graph, periodCtl);
20452
20004
  }
20453
20005
 
@@ -20509,12 +20061,11 @@ ${story_1.splitByNameLinks.toString()}
20509
20061
  }
20510
20062
 
20511
20063
  function startGraph(canvas, searchInput, graph, periodCtl) {
20512
- var allEdges = graph.edges.filter(function (e) {
20513
- return graphState.showWeak || !WEAK_RELS[e.rel];
20514
- });
20515
- // 弱い辺を消したとき、レコード以外の孤立ノードは隠す
20064
+ var allEdges = graph.edges;
20065
+ // レコード以外の点(根拠)は、線が 1 本でもあれば出す。🔴 根拠の点を隠す切り替えは持たない(仕様 graph.evidence-always)。
20066
+ // 消した記録だけが指していた点は線が無いので出ない
20516
20067
  var used = {};
20517
- allEdges.forEach(function (e) { used[e.from] = true; used[e.to] = true; });
20068
+ graph.edges.forEach(function (e) { used[e.from] = true; used[e.to] = true; });
20518
20069
  // ビルド時に算出済みの座標(n.x/n.y)があればそれを使う=ウォームアップ不要で開いた瞬間に整列済み。
20519
20070
  var preLaidOut = graph.nodes.some(function (n) { return typeof n.x === "number"; });
20520
20071
  var nodes = graph.nodes
@@ -20525,6 +20076,8 @@ ${story_1.splitByNameLinks.toString()}
20525
20076
  x: typeof n.x === "number" ? n.x : 0, y: typeof n.y === "number" ? n.y : 0,
20526
20077
  vx: 0, vy: 0, phase: i * 2.399 };
20527
20078
  });
20079
+ // 見えている点の数(開発者ツールとテストが見る。件数の表示はヘッダーから外した)
20080
+ canvas.setAttribute("data-points", String(nodes.length));
20528
20081
  // Gold層を中心に据え、グレーのコンテキストはその外周に配置する(ハブ&スポーク)
20529
20082
  var RING = Math.max(200, 26 * Math.sqrt(nodes.length));
20530
20083
  var byNode = {};
@@ -20975,8 +20528,7 @@ ${story_1.splitByNameLinks.toString()}
20975
20528
  var EDGE_DARK = {
20976
20529
  based_on: "rgba(125,165,255,0.85)", derived_from: "rgba(94,234,212,0.75)",
20977
20530
  relates_to: "rgba(148,163,184,0.6)", supersedes: "rgba(248,113,113,0.9)",
20978
- reference: "rgba(180,196,230,0.32)", source: "rgba(180,196,230,0.32)", link: "rgba(180,196,230,0.22)",
20979
- about: "rgba(196,181,253,0.22)",
20531
+ reference: "rgba(180,196,230,0.6)", source: "rgba(180,196,230,0.6)",
20980
20532
  decided_by: "rgba(244,114,182,0.75)",
20981
20533
  };
20982
20534
  // 画面に入っている範囲(世界座標)。両端が同じ側の外にある線は描かない
@@ -20996,9 +20548,9 @@ ${story_1.splitByNameLinks.toString()}
20996
20548
  var span = Math.max(highO - lowO, 1) * SWEEP_STEP_MS;
20997
20549
  var prog = Math.max(0, Math.min(1, (sweepT - lowO * SWEEP_STEP_MS) / span));
20998
20550
  if (prog > 0) {
20999
- ctx.globalAlpha = WEAK_RELS[e.rel] ? 0.7 : 0.95;
20551
+ ctx.globalAlpha = 0.95;
21000
20552
  ctx.strokeStyle = "#fcd34d";
21001
- ctx.lineWidth = (e.rel === "supersedes" ? 2 : WEAK_RELS[e.rel] ? 1 : 1.6) / view.scale;
20553
+ ctx.lineWidth = (e.rel === "supersedes" ? 2 : 1.6) / view.scale;
21002
20554
  ctx.beginPath();
21003
20555
  ctx.moveTo(lowN.x, lowN.y);
21004
20556
  ctx.lineTo(lowN.x + (highN.x - lowN.x) * prog, lowN.y + (highN.y - lowN.y) * prog);
@@ -21007,18 +20559,18 @@ ${story_1.splitByNameLinks.toString()}
21007
20559
  } else {
21008
20560
  ctx.globalAlpha = 0.06; // リネージ外は沈める
21009
20561
  ctx.strokeStyle = EDGE_DARK[e.rel] || "rgba(180,196,230,0.3)";
21010
- ctx.lineWidth = (WEAK_RELS[e.rel] ? 1 : 1.6) / view.scale;
20562
+ ctx.lineWidth = 1.6 / view.scale;
21011
20563
  ctx.beginPath(); ctx.moveTo(e.a.x, e.a.y); ctx.lineTo(e.b.x, e.b.y); ctx.stroke();
21012
20564
  }
21013
20565
  return;
21014
20566
  }
21015
20567
  // 通常 / 検索
21016
20568
  var hot = searching && (matched[e.a.id] || matched[e.b.id]);
21017
- var a = hot ? 0.95 : (WEAK_RELS[e.rel] ? 0.5 : 0.85);
20569
+ var a = hot ? 0.95 : 0.85;
21018
20570
  if (searching && !hot) a = 0.06;
21019
20571
  ctx.globalAlpha = a;
21020
20572
  ctx.strokeStyle = hot ? "#fcd34d" : (EDGE_DARK[e.rel] || "rgba(180,196,230,0.3)");
21021
- ctx.lineWidth = (hot ? 1.8 : e.rel === "supersedes" ? 2 : WEAK_RELS[e.rel] ? 1 : 1.6) / view.scale;
20573
+ ctx.lineWidth = (hot ? 1.8 : e.rel === "supersedes" ? 2 : 1.6) / view.scale;
21022
20574
  ctx.beginPath();
21023
20575
  ctx.moveTo(e.a.x, e.a.y);
21024
20576
  ctx.lineTo(e.b.x, e.b.y);
@@ -21030,7 +20582,7 @@ ${story_1.splitByNameLinks.toString()}
21030
20582
  var cw = canvas.width, ch = canvas.height;
21031
20583
  ctx.setTransform(1, 0, 0, 1, 0, 0);
21032
20584
  nodes.forEach(function (n) {
21033
- var style = GROUP_STYLE[n.group] || GROUP_STYLE.phantom;
20585
+ var style = GROUP_STYLE[n.group] || GROUP_FALLBACK;
21034
20586
  var alphaN = 1;
21035
20587
  if (searching && !lit[n.id]) alphaN = 0.12;
21036
20588
  if (hovered) {
@@ -21311,7 +20863,7 @@ ${story_1.splitByNameLinks.toString()}
21311
20863
  (adjDir[e.from] = adjDir[e.from] || []).push({ id: e.to, rel: e.rel, dir: "out" });
21312
20864
  (adjDir[e.to] = adjDir[e.to] || []).push({ id: e.from, rel: e.rel, dir: "in" });
21313
20865
  });
21314
- var REL_ORDER = ["supersedes", "based_on", "derived_from", "relates_to", "decided_by", "source", "reference", "link", "about"];
20866
+ var REL_ORDER = ["supersedes", "based_on", "derived_from", "relates_to", "decided_by", "source", "reference"];
21315
20867
 
21316
20868
  function recordText(rec) {
21317
20869
  if (!rec) return "";
@@ -21323,11 +20875,17 @@ ${story_1.splitByNameLinks.toString()}
21323
20875
 
21324
20876
  // Gold層のフォルダ(1列目)。検索ヒット表示と使い分ける
21325
20877
  var goldGroups = [];
21326
- if (SITE.home) goldGroups.push({ name: "Home", ids: [SITE.home.id] });
21327
- // 削除済みは列にも件数にも出さない(一覧・グラフと同じ扱いに揃える)
21328
- goldGroups.push({ name: "Decisions", ids: visibleRecords(SITE.decisions).map(function (d) { return d.id; }) });
20878
+ // 削除済みは列にも件数にも出さない(一覧・グラフと同じ扱いに揃える)。
20879
+ // 🔴 点にならない記録(Home・レポート・その他)は出さない——グラフの点(nodeById)にある記録だけ
20880
+ var inGraph = {};
20881
+ (explorerGraph.nodes || []).forEach(function (n) { if (n.recordId) inGraph[n.recordId] = true; });
20882
+ function graphIds(list) {
20883
+ return visibleRecords(list).map(function (r) { return r.id; }).filter(function (id) { return inGraph[id]; });
20884
+ }
20885
+ goldGroups.push({ name: "Decisions", ids: graphIds(SITE.decisions) });
21329
20886
  (SITE.sections || []).forEach(function (sec) {
21330
- goldGroups.push({ name: sec.name, ids: visibleRecords(sec.records).map(function (r) { return r.id; }) });
20887
+ var ids = graphIds(sec.records);
20888
+ if (ids.length) goldGroups.push({ name: sec.name, ids: ids });
21331
20889
  });
21332
20890
 
21333
20891
  function renderExplorer() {
@@ -21369,7 +20927,7 @@ ${story_1.splitByNameLinks.toString()}
21369
20927
  app.appendChild(wrap);
21370
20928
 
21371
20929
  function nodeDot(n) {
21372
- var style = GROUP_STYLE[n.group] || GROUP_STYLE.phantom;
20930
+ var style = GROUP_STYLE[n.group] || GROUP_FALLBACK;
21373
20931
  var dot = el("span", { class: "mc-dot" });
21374
20932
  dot.style.background = style.color;
21375
20933
  return dot;
@@ -21533,10 +21091,8 @@ ${story_1.splitByNameLinks.toString()}
21533
21091
  path.setAttribute("d", "M " + x1 + " " + y1 + " C " + mx + " " + y1 + ", " + mx + " " + y2 + ", " + x2 + " " + y2);
21534
21092
  path.setAttribute("fill", "none");
21535
21093
  path.setAttribute("stroke", REL_COLOR[rows[j].getAttribute("data-rel")] || "#cbd5e1");
21536
- // 話題の線は細く薄く(名前の一致の手がかり・根拠の線より弱い)
21537
- var weakLine = rows[j].getAttribute("data-rel") === "about";
21538
- path.setAttribute("stroke-width", weakLine ? "1" : "1.5");
21539
- path.setAttribute("opacity", weakLine ? "0.5" : "0.85");
21094
+ path.setAttribute("stroke-width", "1.5");
21095
+ path.setAttribute("opacity", "0.85");
21540
21096
  lines.appendChild(path);
21541
21097
  }
21542
21098
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takagaki/cortex-decisions-viewer",
3
- "version": "0.12.160",
3
+ "version": "0.12.163",
4
4
  "description": "Cortexの意思決定記録(Decisions/*.md)を静的サイトにビルドして閲覧する。各レコードに「Edit on GitHub」リンクを付与する。",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,