@takagaki/cortex-decisions-viewer 0.12.16 → 0.12.18
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 +148 -5
- package/dist/render.js +1305 -85
- package/package.json +4 -2
package/dist/build.js
CHANGED
|
@@ -36,7 +36,9 @@ 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.sanitizeBody = sanitizeBody;
|
|
39
40
|
exports.readFleetSources = readFleetSources;
|
|
41
|
+
exports.readHarnessSchedules = readHarnessSchedules;
|
|
40
42
|
exports.readGoldProposals = readGoldProposals;
|
|
41
43
|
exports.buildHomeGrounding = buildHomeGrounding;
|
|
42
44
|
exports.collectMeetings = collectMeetings;
|
|
@@ -47,6 +49,7 @@ const node_fs_1 = require("node:fs");
|
|
|
47
49
|
const path = __importStar(require("node:path"));
|
|
48
50
|
const gray_matter_1 = __importDefault(require("gray-matter"));
|
|
49
51
|
const marked_1 = require("marked");
|
|
52
|
+
const sanitize_html_1 = __importDefault(require("sanitize-html"));
|
|
50
53
|
const repo_1 = require("./repo");
|
|
51
54
|
// 直近のGold昇格で追加されたレコードのパス集合。レコード生成の各所から参照するのでモジュールスコープに置く
|
|
52
55
|
// (引数で引き回すと呼び出し側の改修が広範囲になる)。build() の冒頭で1回だけ解決する。
|
|
@@ -102,6 +105,88 @@ function toRelations(value) {
|
|
|
102
105
|
function externalLinksToNewTab(html) {
|
|
103
106
|
return html.replace(/<a href="(https?:\/\/[^"]*)"/g, '<a target="_blank" rel="noopener" href="$1"');
|
|
104
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* 本文HTMLで許可する構造。**Markdown(marked)が生成しうるものだけ**を列挙したホワイトリスト。
|
|
110
|
+
* ここに無いタグ・属性は落ちる(`<script>`・`on*` ハンドラ・`javascript:` 等)。
|
|
111
|
+
*/
|
|
112
|
+
const SANITIZE_OPTIONS = {
|
|
113
|
+
allowedTags: [
|
|
114
|
+
"p", "br", "hr",
|
|
115
|
+
"h1", "h2", "h3", "h4", "h5", "h6",
|
|
116
|
+
"strong", "em", "del", "code", "pre",
|
|
117
|
+
"ul", "ol", "li",
|
|
118
|
+
"blockquote",
|
|
119
|
+
"a", "img",
|
|
120
|
+
"table", "thead", "tbody", "tr", "th", "td",
|
|
121
|
+
// タスクリスト `- [ ]` / `- [x]` のチェック状態を残すのに要る。
|
|
122
|
+
// 落とすと**装飾ではなく情報(未完了/完了の別)が消える**
|
|
123
|
+
"input",
|
|
124
|
+
],
|
|
125
|
+
allowedAttributes: {
|
|
126
|
+
a: ["href", "target", "rel"],
|
|
127
|
+
img: ["src", "alt", "title"], // src の許容スキームは allowedSchemesByTag で絞る
|
|
128
|
+
input: ["type", "checked", "disabled"],
|
|
129
|
+
},
|
|
130
|
+
allowedSchemes: ["http", "https", "mailto"],
|
|
131
|
+
allowedSchemesByTag: {
|
|
132
|
+
// 🔴 img は http(s) を**許可しない**。この画面は顧客が見るので、ブラウザが自動で
|
|
133
|
+
// 取りに行く外部参照を置くと「誰がいつ見たか」が参照先のログに残る
|
|
134
|
+
// (同じ理由で test/no-external-fetch.test.mjs が img を名指しで禁じている)。
|
|
135
|
+
// 埋め込みの data: 画像はネットワークに出ないので通す(実データに実在する)
|
|
136
|
+
img: ["data"],
|
|
137
|
+
},
|
|
138
|
+
transformTags: {
|
|
139
|
+
// data: のうち画像以外(data:text/html 等)を弾く。
|
|
140
|
+
// allowedSchemesByTag はスキーム名しか見ないので、その先を見る二重の防御
|
|
141
|
+
img: (tagName, attribs) => {
|
|
142
|
+
if (attribs.src && !/^data:image\//.test(attribs.src)) {
|
|
143
|
+
const { src, ...rest } = attribs;
|
|
144
|
+
return { tagName, attribs: rest };
|
|
145
|
+
}
|
|
146
|
+
return { tagName, attribs };
|
|
147
|
+
},
|
|
148
|
+
// checkbox 以外の input は本文で意味を持たないので無力化する
|
|
149
|
+
input: (tagName, attribs) => attribs.type === "checkbox" ? { tagName, attribs } : { tagName: "span", attribs: {} },
|
|
150
|
+
},
|
|
151
|
+
// 既定値だが明示する。タグを消して中身のテキストだけ残す("escape" だと未対応のタグが
|
|
152
|
+
// 文字として画面に見える)。落ち方は test/sanitize-body.test.mjs で固定してある
|
|
153
|
+
disallowedTagsMode: "discard",
|
|
154
|
+
// 既定値だが明示する。`//host/` 形式は通るが、img は上でスキームを data: のみに
|
|
155
|
+
// 絞っているため弾かれ、a は元々 http(s) 外部リンクを許可する設計なので実益がない
|
|
156
|
+
allowProtocolRelative: true,
|
|
157
|
+
};
|
|
158
|
+
/**
|
|
159
|
+
* Markdown由来のHTMLを、閲覧者のブラウザに渡す前に無害化する。
|
|
160
|
+
*
|
|
161
|
+
* 案件リポジトリの本文(Slack・Backlog・議事録からの転記)は書き込み時の承認フローが無く、
|
|
162
|
+
* **ここが唯一の関所**。本文HTMLは render 側で `innerHTML` に流し込まれるため、
|
|
163
|
+
* ビルド時に通していないHTMLが画面へ届く経路を作らないこと。
|
|
164
|
+
*/
|
|
165
|
+
function sanitizeBody(html) {
|
|
166
|
+
return (0, sanitize_html_1.default)(html, SANITIZE_OPTIONS);
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* 詳細ページの本文用。先頭のH1=タイトルは画面が別途表示するので除去する。
|
|
170
|
+
*
|
|
171
|
+
* 🔴 **H1除去を落とさないこと。** 落とすと1,000件超のレコードでタイトルが二重に出る。
|
|
172
|
+
*
|
|
173
|
+
* サニタイズ → target=_blank 付与、の順で適用する。`externalLinksToNewTab()` は
|
|
174
|
+
* 生HTML文字列への正規表現書き換えなので、**サニタイズ後に走らせて初めて**
|
|
175
|
+
* 「`<a href="http…` という並びがタグ以外の場所に現れない」という前提が保証される
|
|
176
|
+
* (サニタイズ済みHTMLはテキスト・属性値中の `<` `"` をエスケープ済み)。
|
|
177
|
+
*/
|
|
178
|
+
async function renderRecordBody(markdown) {
|
|
179
|
+
const html = sanitizeBody(await marked_1.marked.parse(markdown, { async: true }))
|
|
180
|
+
.replace(/^\s*<h1[^>]*>[\s\S]*?<\/h1>\s*/, "");
|
|
181
|
+
return externalLinksToNewTab(html);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Home.mdの自己記述セクション用。見出しはbuild側が別途付与するのでH1除去は要らない。
|
|
185
|
+
* 順序の理由は renderRecordBody と同じ。
|
|
186
|
+
*/
|
|
187
|
+
async function renderGroundingSection(markdown) {
|
|
188
|
+
return externalLinksToNewTab(sanitizeBody(await marked_1.marked.parse(markdown, { async: true })));
|
|
189
|
+
}
|
|
105
190
|
/**
|
|
106
191
|
* 本文markdownを、検索用の軽量plaintextに変換する。
|
|
107
192
|
* リッチHTMLはbodyRefへ外出しするため、inlineデータには検索できる最小限のテキストだけを残す。
|
|
@@ -362,10 +447,26 @@ async function readFleetSources(repoRoot) {
|
|
|
362
447
|
const state = c.state != null ? String(c.state) : "";
|
|
363
448
|
if (!state)
|
|
364
449
|
continue;
|
|
450
|
+
// アダプタごとの状態(#153・前方互換キー)。単数形は broken > on > その他 の代表値なので、
|
|
451
|
+
// 複数アダプタの能力ではこちらが各タイルの正本になる。**ここも明示の許可リスト**——
|
|
452
|
+
// 列挙し忘れたフィールドは静かに消える(このファイル冒頭の罠と同じ)。
|
|
453
|
+
// 無い・空・要素が壊れている(adapter か state を欠く)ときは載せず、
|
|
454
|
+
// 単数形の代表値へ倒す(矛盾した載せ方をされても行が消えない側に倒す)
|
|
455
|
+
const ads = Array.isArray(c.adapters)
|
|
456
|
+
? c.adapters
|
|
457
|
+
.filter((a) => a != null && typeof a === "object")
|
|
458
|
+
.map((a) => ({
|
|
459
|
+
adapter: a.adapter != null ? String(a.adapter) : "",
|
|
460
|
+
state: a.state != null ? String(a.state) : "",
|
|
461
|
+
detail: a.detail != null ? String(a.detail) : undefined,
|
|
462
|
+
}))
|
|
463
|
+
.filter((a) => a.adapter !== "" && a.state !== "")
|
|
464
|
+
: [];
|
|
365
465
|
out[cap] = {
|
|
366
466
|
adapter: c.adapter != null ? String(c.adapter) : "",
|
|
367
467
|
state,
|
|
368
468
|
detail: c.detail != null ? String(c.detail) : undefined,
|
|
469
|
+
...(ads.length ? { adapters: ads } : {}),
|
|
369
470
|
};
|
|
370
471
|
}
|
|
371
472
|
if (Object.keys(out).length)
|
|
@@ -433,6 +534,35 @@ async function readFleetSources(repoRoot) {
|
|
|
433
534
|
}
|
|
434
535
|
return { externalSources, slackWorkspaces, internalSources, pipelines, capabilities, fleetGeneratedAt, secretSources, secretSpaces, checks, githubOwners, backlogTarget };
|
|
435
536
|
}
|
|
537
|
+
/**
|
|
538
|
+
* いまオンになっているハーネスの機能(リポジトリ直下 `ハーネス連携/schedules.json`)を読む。
|
|
539
|
+
*
|
|
540
|
+
* **読み方は readFleetSources と同じ流儀**——素通しはせず、明示の許可リストを通す。
|
|
541
|
+
* このファイルは engine 側(プラン生成)と画面の両方が読む正本で、engine が先に欄を
|
|
542
|
+
* 増やすことがある。**知らないものは画面へ載せない**が唯一の安全側。
|
|
543
|
+
* 篩の実体は render.ts の `sanitizeHarnessSchedules`(画面側の楽観更新と同じ関数を通す
|
|
544
|
+
* ——写経して2枚にすると、片方だけ語彙を増やしたときに静かに食い違う)。
|
|
545
|
+
*
|
|
546
|
+
* **無い・壊れているときは undefined**(=「何もオンでない」と同じ見え方)。
|
|
547
|
+
* 壊れの検知はプラン生成(赤で落ちる)と intake の責務で、画面がそれを兼ねると
|
|
548
|
+
* 「読めないから何も出さない」と「1件もオンでない」の区別が画面に出せない。
|
|
549
|
+
*/
|
|
550
|
+
async function readHarnessSchedules(repoRoot) {
|
|
551
|
+
let json;
|
|
552
|
+
try {
|
|
553
|
+
const raw = await node_fs_1.promises.readFile(path.join(repoRoot, "ハーネス連携", "schedules.json"), "utf8");
|
|
554
|
+
json = JSON.parse(raw);
|
|
555
|
+
}
|
|
556
|
+
catch {
|
|
557
|
+
return undefined; // 無い・壊れている → キーごと出さない
|
|
558
|
+
}
|
|
559
|
+
if (json == null || typeof json !== "object" || Array.isArray(json))
|
|
560
|
+
return undefined;
|
|
561
|
+
const rows = json.schedules;
|
|
562
|
+
if (!Array.isArray(rows))
|
|
563
|
+
return undefined;
|
|
564
|
+
return (0, render_1.sanitizeHarnessSchedules)(rows);
|
|
565
|
+
}
|
|
436
566
|
/**
|
|
437
567
|
* AIからの更新提案(`.cortex/gold-proposals.json`)を読む。
|
|
438
568
|
*
|
|
@@ -596,7 +726,7 @@ async function buildHomeGrounding(rawMarkdown) {
|
|
|
596
726
|
continue;
|
|
597
727
|
sections.push({
|
|
598
728
|
heading: h,
|
|
599
|
-
html:
|
|
729
|
+
html: await renderGroundingSection(hit.body),
|
|
600
730
|
});
|
|
601
731
|
}
|
|
602
732
|
// 未記入の判定。新形式は「この案件について」と「ゴール」の2つ、旧形式は1つの節を見る。
|
|
@@ -618,7 +748,7 @@ async function buildHomeGrounding(rawMarkdown) {
|
|
|
618
748
|
.filter((p) => !(p.heading != null && dropped.has(p.heading)))
|
|
619
749
|
.map((p) => (p.heading == null ? p.body : `## ${p.heading}\n${p.body}`))
|
|
620
750
|
.join("\n");
|
|
621
|
-
const restHtml =
|
|
751
|
+
const restHtml = await renderRecordBody(rest);
|
|
622
752
|
return { sections, unfilled, restHtml };
|
|
623
753
|
}
|
|
624
754
|
/** パスがディレクトリとして存在するか */
|
|
@@ -672,7 +802,7 @@ async function parseGeneric(raw, fileName, repoRelDir, repoBaseUrl, branch) {
|
|
|
672
802
|
: data.summary != null
|
|
673
803
|
? String(data.summary)
|
|
674
804
|
: "";
|
|
675
|
-
const bodyHtml =
|
|
805
|
+
const bodyHtml = await renderRecordBody(content);
|
|
676
806
|
const filePath = repoRelDir ? `${repoRelDir}/${fileName}` : fileName;
|
|
677
807
|
const editUrl = repoBaseUrl
|
|
678
808
|
? `${repoBaseUrl}/edit/${branch}/${encodePath(filePath)}`
|
|
@@ -1619,6 +1749,8 @@ async function build(opts) {
|
|
|
1619
1749
|
computeLayout(graph); // 各ノードに x/y を書き込む(クライアントはこれを初期位置に使いウォームアップ省略)
|
|
1620
1750
|
// データソースと自動化(fleet-status.json 由来)。無ければ undefined でセクション非表示。
|
|
1621
1751
|
const fleet = await readFleetSources(repoRoot);
|
|
1752
|
+
// いまオンになっているハーネスの機能。無ければ undefined でタイルは「すべてオフ」
|
|
1753
|
+
const harnessSchedules = await readHarnessSchedules(repoRoot);
|
|
1622
1754
|
// すでにある会議(投入モーダルの「会議名」の選択肢)。intake の有無に関わらず載せる——
|
|
1623
1755
|
// 出し分けは表示側の仕事で、ここで絞ると「投入の口を後から付けたら選択肢が空」になる
|
|
1624
1756
|
const meetings = await collectMeetings(repoRoot);
|
|
@@ -1669,6 +1801,10 @@ async function build(opts) {
|
|
|
1669
1801
|
checks: fleet.checks,
|
|
1670
1802
|
githubOwners: fleet.githubOwners,
|
|
1671
1803
|
backlogTarget: fleet.backlogTarget,
|
|
1804
|
+
// **短縮記法(`harnessSchedules,`)にしないこと。** fleet-passthrough のテストが
|
|
1805
|
+
// 「画面が読む欄を build.ts が受け渡しているか」を `名前:` の字面で走査しており、
|
|
1806
|
+
// 短縮記法にすると、その守りの外へ静かに落ちる
|
|
1807
|
+
harnessSchedules: harnessSchedules,
|
|
1672
1808
|
// null(無い・壊れ・version違い)は undefined へ落とす——SiteData の
|
|
1673
1809
|
// 「知らないことはキーが無いことで表す」に合わせる(空の器を通さない)
|
|
1674
1810
|
// 空の器は通さない(0件なら undefined = キーごと出さない)
|
|
@@ -1715,7 +1851,7 @@ async function parseDecision(raw, fileName, repoRelDir, repoBaseUrl, branch, ref
|
|
|
1715
1851
|
const references = referencesRaw.map((r) => refs.resolve(r));
|
|
1716
1852
|
const relations = toRelations(data.relations);
|
|
1717
1853
|
// 本文先頭の H1 はタイトルと重複するため除去(詳細画面ではタイトルを別途表示する)
|
|
1718
|
-
const bodyHtml =
|
|
1854
|
+
const bodyHtml = await renderRecordBody(content);
|
|
1719
1855
|
const filePath = repoRelDir ? `${repoRelDir}/${fileName}` : fileName;
|
|
1720
1856
|
const editUrl = repoBaseUrl
|
|
1721
1857
|
? `${repoBaseUrl}/edit/${branch}/${encodePath(filePath)}`
|
|
@@ -1772,8 +1908,15 @@ function createRefResolver(opts) {
|
|
|
1772
1908
|
if (!trimmed)
|
|
1773
1909
|
return { label: trimmed, url: null };
|
|
1774
1910
|
const mdLink = trimmed.match(/^\[([^\]]+)\]\(([^)]+)\)$/);
|
|
1775
|
-
if (mdLink)
|
|
1911
|
+
if (mdLink) {
|
|
1912
|
+
// 🔴 http(s) 以外(`javascript:` `data:` 等)はリンクにしない。ラベルだけ残す。
|
|
1913
|
+
// **ここは references の表示だけの話ではない。** この url はグラフのノードにも
|
|
1914
|
+
// そのまま載り(addReferenceEdges)、ノードを押すと `window.open(n.url)` される。
|
|
1915
|
+
// 描画側にスキーム検査は無いので、**この1箇所を緩めるとグラフ経由の穴が静かに開く**
|
|
1916
|
+
if (!/^https?:\/\//.test(mdLink[2]))
|
|
1917
|
+
return { label: mdLink[1], url: null };
|
|
1776
1918
|
return { label: mdLink[1], url: mdLink[2] };
|
|
1919
|
+
}
|
|
1777
1920
|
if (/^https?:\/\//.test(trimmed))
|
|
1778
1921
|
return { label: trimmed, url: trimmed };
|
|
1779
1922
|
// 正本のURL(課題キー・material:・design:・minute:)。**ここが「ツールへ飛ぶ」の本体**
|