@takagaki/cortex-decisions-viewer 0.12.19 → 0.12.21
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/README.md +17 -2
- package/dist/build.js +32 -3
- package/dist/render.js +49 -4
- package/dist/repo.js +18 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Cortexの意思決定記録(`Decisions/*.md`)を**静的サイトにビルドして閲覧する**CLIツール。
|
|
4
4
|
各レコードに「**Edit on GitHub**」リンクを付与し、閲覧画面から直接GitHubのエディタで編集できる。
|
|
5
5
|
|
|
6
|
-
`md-meta-view` の代替として、Cortex
|
|
6
|
+
`md-meta-view` の代替として、Cortex専用に自前実装したもの。出力は外部依存もサーバーも要らない静的ファイル一式で、AWS Amplify Hosting や S3 + CloudFront にそのまま載る。**JS と CSS は HTML に埋め込まず、`assets/` の外部ファイルに出す**——配信側(AIS ポータル)の Content-Security-Policy がインラインの `<script>` / `<style>` を実行しないため([#301](https://github.com/classmethod-internal/cortex-tools/issues/301))。
|
|
7
7
|
|
|
8
8
|
## 使い方
|
|
9
9
|
|
|
@@ -13,7 +13,22 @@ Cortexの意思決定記録(`Decisions/*.md`)を**静的サイトにビル
|
|
|
13
13
|
npx -y @takagaki/cortex-decisions-viewer build --out site --title "Decisions"
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
ビルド結果は `site
|
|
16
|
+
ビルド結果は `site/` ディレクトリ一式。`?id=<id>` で個別レコードに直リンクできる。
|
|
17
|
+
|
|
18
|
+
| 出力 | 中身 |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| `index.html` | 画面の骨組みと、埋め込みデータ(`<script type="application/json">`) |
|
|
21
|
+
| `404.html` | `index.html` と同じもの(サブパス直叩きのフォールバック) |
|
|
22
|
+
| `assets/viewer.<内容ハッシュ>.js` / `.css` | 画面のJSとCSS。内容が同じなら名前も同じ(版が同じなら全案件で同じ名前) |
|
|
23
|
+
| `bodies/*.html` | レコードの本文(詳細を開いたときに取りに行く) |
|
|
24
|
+
| `thumbs/*` | グラフのサムネイル画像 |
|
|
25
|
+
|
|
26
|
+
**配置は `site/` を丸ごと置く**(`index.html` だけを置いても画面は出ない)。
|
|
27
|
+
ファイル名を内容ハッシュにしているのは、`Cache-Control` を付けずに置かれた
|
|
28
|
+
`index.html` をブラウザが発見的にキャッシュするため——固定名だと
|
|
29
|
+
「利用者の手元に残った古い `index.html` が、消えた JS を指す」形になり、
|
|
30
|
+
強制再読み込みまで画面が真っ白になる。中身が変われば URL も変わるなら起きない。
|
|
31
|
+
配置側では `index.html` / `404.html` を `no-cache`、`assets/` を長期キャッシュにするとよい。
|
|
17
32
|
|
|
18
33
|
## CLI
|
|
19
34
|
|
package/dist/build.js
CHANGED
|
@@ -55,6 +55,16 @@ const repo_1 = require("./repo");
|
|
|
55
55
|
// 直近のGold昇格で追加されたレコードのパス集合。レコード生成の各所から参照するのでモジュールスコープに置く
|
|
56
56
|
// (引数で引き回すと呼び出し側の改修が広範囲になる)。build() の冒頭で1回だけ解決する。
|
|
57
57
|
let LATEST_PATHS = new Set();
|
|
58
|
+
/**
|
|
59
|
+
* このレコードが「直近のGold昇格で追加されたもの」か。
|
|
60
|
+
*
|
|
61
|
+
* **照合の手前で NFC に正規化する。** 集合側(git 由来)は NFC で入っているが、
|
|
62
|
+
* こちらのパスは readdir 由来なので、ファイルシステムが NFD で返す環境(macOS の
|
|
63
|
+
* 一部の経路)では**同じ日本語名でも一致しない**。正規化はこの1箇所に集める。
|
|
64
|
+
*/
|
|
65
|
+
function isLatestPath(filePath) {
|
|
66
|
+
return LATEST_PATHS.has(filePath.normalize("NFC"));
|
|
67
|
+
}
|
|
58
68
|
// AIからの更新提案(`.cortex/gold-proposals.json` 由来)を、レコードのパスで引ける形にしたもの。
|
|
59
69
|
// **LATEST_PATHS と同じ置き方にしている**——どちらも「レコードを組み立てるその場で、
|
|
60
70
|
// リポジトリルート相対のパスを鍵に引く」もので、引き回すと parseDecision / parseGeneric の
|
|
@@ -849,7 +859,7 @@ async function parseGeneric(raw, fileName, repoRelDir, repoBaseUrl, branch) {
|
|
|
849
859
|
editUrl,
|
|
850
860
|
rtype: data.type != null ? String(data.type) : "",
|
|
851
861
|
rstatus: data.status != null ? String(data.status) : "",
|
|
852
|
-
isLatest:
|
|
862
|
+
isLatest: isLatestPath(filePath),
|
|
853
863
|
proposals: takeProposals(filePath),
|
|
854
864
|
relations: toRelations(data.relations),
|
|
855
865
|
source: data.source != null ? String(data.source) : "",
|
|
@@ -1878,7 +1888,26 @@ async function build(opts) {
|
|
|
1878
1888
|
// 本文HTML(リッチ・かさばる)を bodies/ へ外出しし、inlineデータからは除外する。
|
|
1879
1889
|
// 一覧・グラフ・検索は inline の searchText(軽量plaintext)で動き、詳細表示時にbodyRefをfetchする。
|
|
1880
1890
|
await externalizeBodies(site, outAbs);
|
|
1881
|
-
|
|
1891
|
+
// CSSとJSは外部ファイルへ出す。**インラインのままだと配信先で画面が出ない**——
|
|
1892
|
+
// AISポータルは /p/<案件>/cm/ に厳格な Content-Security-Policy を返しており、
|
|
1893
|
+
// インラインの style / script を実行しない(cortex-tools #301)。
|
|
1894
|
+
//
|
|
1895
|
+
// ファイル名を内容ハッシュにする理由: 配置先の S3 は Cache-Control を付けずに
|
|
1896
|
+
// 置かれ、ブラウザは Last-Modified からの発見的キャッシュをする。固定名だと
|
|
1897
|
+
// 「利用者の手元に残った古い index.html が、消えた JS を指す」形になり、
|
|
1898
|
+
// 強制再読み込みまで真っ白になる。中身が違えば URL も違うなら、それが起きない。
|
|
1899
|
+
//
|
|
1900
|
+
// 配置側(cortex-engine の build-viewer.yml)の責務: aws s3 sync --delete が
|
|
1901
|
+
// 古いハッシュのファイルを消すこと、index.html に Cache-Control: no-cache を
|
|
1902
|
+
// 付けること(assets/ は内容ハッシュ名なので長期キャッシュでよい)。
|
|
1903
|
+
const assetSources = (0, render_1.viewerAssetSources)();
|
|
1904
|
+
const assets = (0, render_1.viewerAssetNames)(assetSources);
|
|
1905
|
+
await node_fs_1.promises.mkdir(path.join(outAbs, "assets"), { recursive: true });
|
|
1906
|
+
await node_fs_1.promises.writeFile(path.join(outAbs, assets.css), assetSources.css, "utf8");
|
|
1907
|
+
await node_fs_1.promises.writeFile(path.join(outAbs, assets.js), assetSources.js, "utf8");
|
|
1908
|
+
// **renderSite ではなく renderSiteWithAssets を呼ぶ。** 引数を1つ落としても
|
|
1909
|
+
// 型が通ってしまう形にすると、本番の出力が黙ってインラインに戻る
|
|
1910
|
+
const html = (0, render_1.renderSiteWithAssets)(site, assets);
|
|
1882
1911
|
await node_fs_1.promises.writeFile(path.join(outAbs, "index.html"), html, "utf8");
|
|
1883
1912
|
// SPA的フォールバック(?id= はクエリなので不要だが、サブパス直叩き対策として404も用意)
|
|
1884
1913
|
await node_fs_1.promises.writeFile(path.join(outAbs, "404.html"), html, "utf8");
|
|
@@ -1918,7 +1947,7 @@ async function parseDecision(raw, fileName, repoRelDir, repoBaseUrl, branch, ref
|
|
|
1918
1947
|
bodyRef: "",
|
|
1919
1948
|
searchText: toSearchText(content),
|
|
1920
1949
|
rstatus: data.status != null ? String(data.status) : "",
|
|
1921
|
-
isLatest:
|
|
1950
|
+
isLatest: isLatestPath(filePath),
|
|
1922
1951
|
proposals: takeProposals(filePath),
|
|
1923
1952
|
fileName,
|
|
1924
1953
|
editUrl,
|
package/dist/render.js
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.UNSETTLED_LIMITS = exports.LIVE_CONFIRM_LIMITS = void 0;
|
|
4
4
|
exports.renderSite = renderSite;
|
|
5
|
+
exports.renderSiteWithAssets = renderSiteWithAssets;
|
|
6
|
+
exports.viewerAssetSources = viewerAssetSources;
|
|
7
|
+
exports.viewerAssetNames = viewerAssetNames;
|
|
5
8
|
exports.sanitizeRegisteredNow = sanitizeRegisteredNow;
|
|
6
9
|
exports.sanitizeHarnessSchedules = sanitizeHarnessSchedules;
|
|
7
10
|
exports.sanitizeHarnessShelf = sanitizeHarnessShelf;
|
|
@@ -29,9 +32,19 @@ exports.ingestedAtOf = ingestedAtOf;
|
|
|
29
32
|
exports.keyMissingText = keyMissingText;
|
|
30
33
|
exports.deriveCapTile = deriveCapTile;
|
|
31
34
|
exports.renderConnectionMap = renderConnectionMap;
|
|
35
|
+
const node_crypto_1 = require("node:crypto");
|
|
32
36
|
const meet_guide_images_1 = require("./meet-guide-images");
|
|
33
|
-
/**
|
|
34
|
-
|
|
37
|
+
/**
|
|
38
|
+
* SiteDataをHTMLにレンダリングする。
|
|
39
|
+
*
|
|
40
|
+
* `assets` を渡すと、CSSとJSを**外部ファイルへの参照**(link/script src)で出す。
|
|
41
|
+
* 配信側(AISポータル)の Content-Security-Policy がインラインの style / script を
|
|
42
|
+
* 実行しないため(cortex-tools #301)。本番の出力(build())は必ずこちら。
|
|
43
|
+
*
|
|
44
|
+
* `assets` 省略時は従来どおり単一の自己完結HTML(埋め込みデータ+CSS+JS)を返す。
|
|
45
|
+
* JSDOM で画面を動かす既存テストがこの形に依存している(外部ファイルは読まれない)。
|
|
46
|
+
*/
|
|
47
|
+
function renderSite(site, assets) {
|
|
35
48
|
// </script> によるブレイクアウトを防ぐため < をエスケープして埋め込む
|
|
36
49
|
// Claude Code Web ディープリンク(この案件リポ+狙ったプロンプトで開く)。repoBaseUrl から slug を導出(追加入力なし)。
|
|
37
50
|
const repoSlug = site.repoBaseUrl
|
|
@@ -54,6 +67,13 @@ function renderSite(site) {
|
|
|
54
67
|
? `<a class="gh-btn" href="${escapeHtml(site.repoBaseUrl)}" target="_blank" rel="noopener" title="コンテキストリポジトリをGitHubで開く">${GITHUB_ICON}</a>`
|
|
55
68
|
: "";
|
|
56
69
|
const askBtn = `<a class="ask-ai-btn" href="${escapeHtml(askUrl)}" target="_blank" rel="noopener" title="Claude Code Web でこの案件のリポジトリを開き、プロジェクトについて質問・調査できます">💬 プロジェクトについてAIに聞く</a>`;
|
|
70
|
+
// CSPでインラインが拒否される環境向けの外部参照形。省略時は従来どおり埋め込む
|
|
71
|
+
const styleTag = assets
|
|
72
|
+
? `<link rel="stylesheet" href="${escapeHtml(assets.css)}" />`
|
|
73
|
+
: `<style>${CSS}</style>`;
|
|
74
|
+
const scriptTag = assets
|
|
75
|
+
? `<script src="${escapeHtml(assets.js)}"></script>`
|
|
76
|
+
: `<script>${CLIENT_JS}</script>`;
|
|
57
77
|
return `<!DOCTYPE html>
|
|
58
78
|
<html lang="ja">
|
|
59
79
|
<head>
|
|
@@ -61,7 +81,7 @@ function renderSite(site) {
|
|
|
61
81
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
62
82
|
<meta name="robots" content="noindex, nofollow" />
|
|
63
83
|
<title>${safeTitle}</title>
|
|
64
|
-
|
|
84
|
+
${styleTag}
|
|
65
85
|
</head>
|
|
66
86
|
<body>
|
|
67
87
|
<header class="site-header">
|
|
@@ -77,7 +97,7 @@ function renderSite(site) {
|
|
|
77
97
|
</footer>
|
|
78
98
|
${connMap ? `<template id="conn-map">${connMap}</template>\n` : ""}<script type="application/json" id="tutorial-data">${tutorialJson}</script>
|
|
79
99
|
<script type="application/json" id="site-data">${dataJson}</script>
|
|
80
|
-
|
|
100
|
+
${scriptTag}
|
|
81
101
|
</body>
|
|
82
102
|
</html>`;
|
|
83
103
|
}
|
|
@@ -89,6 +109,31 @@ function escapeHtml(s) {
|
|
|
89
109
|
.replace(/>/g, ">")
|
|
90
110
|
.replace(/"/g, """);
|
|
91
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* 外部ファイル参照の形でレンダリングする(assets必須)。
|
|
114
|
+
*
|
|
115
|
+
* build() はこちらだけを呼ぶ。renderSite は assets を省略できるので、
|
|
116
|
+
* 本番の出力にインラインが戻る退行を型で止められない。その1点のための薄い関数。
|
|
117
|
+
*/
|
|
118
|
+
function renderSiteWithAssets(site, assets) {
|
|
119
|
+
return renderSite(site, assets);
|
|
120
|
+
}
|
|
121
|
+
/** 外部ファイルへ書き出す中身。埋め込み形(renderSite の assets 省略時)と同一の文字列 */
|
|
122
|
+
function viewerAssetSources() {
|
|
123
|
+
return { css: CSS, js: CLIENT_JS };
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* 中身から外部ファイル名を決める。
|
|
127
|
+
*
|
|
128
|
+
* 内容ハッシュ(sha256の先頭8桁)にするのは、配信先がブラウザのキャッシュを
|
|
129
|
+
* 持つため。固定名だと「古いHTMLと新しいJS」の組み合わせが起こりうるが、
|
|
130
|
+
* 中身が変われば名前も変わるなら起こらない。**同じ中身なら同じ名前**なので、
|
|
131
|
+
* 版が同じかぎり全案件で同じファイル名になる。
|
|
132
|
+
*/
|
|
133
|
+
function viewerAssetNames(sources) {
|
|
134
|
+
const h = (text) => (0, node_crypto_1.createHash)("sha256").update(text, "utf8").digest("hex").slice(0, 8);
|
|
135
|
+
return { css: `assets/viewer.${h(sources.css)}.css`, js: `assets/viewer.${h(sources.js)}.js` };
|
|
136
|
+
}
|
|
92
137
|
/** 接続マップの能力定義(表示順・絵文字は詳細カードの流儀を踏襲) */
|
|
93
138
|
/**
|
|
94
139
|
* アダプタのロゴ。**公式配布のSVGをそのまま埋め込む。**
|
package/dist/repo.js
CHANGED
|
@@ -33,13 +33,22 @@ function resolveRepo(opts) {
|
|
|
33
33
|
* 最新コミットと同じ「日」のパイプラインコミットまでをまとめて1回の実行として扱う。
|
|
34
34
|
*
|
|
35
35
|
* clone が浅い(--depth 1 等)・git が無い環境では**空集合を返す**(ラベルが出ないだけで壊れない)。
|
|
36
|
+
*
|
|
37
|
+
* 🔴 **パスは `-z`(NUL区切り)で取り、NFC に正規化してから集合に入れる。**
|
|
38
|
+
* - `-z` … 既定の git(`core.quotePath=true`)は非ASCIIを含むパスを
|
|
39
|
+
* `"Cortex/Rules/records/\345\261\245…md"` と引用符+8進エスケープで出す。レコードは
|
|
40
|
+
* **日本語のファイル名**なので、そのまま入れると照合側(readdir の生 UTF-8)と
|
|
41
|
+
* 一致せず「最新」ラベルが**1件も出なくなる**(#328 で実際に踏んだ)。
|
|
42
|
+
* - NFC … リポジトリのパスは NFC が正(cortex-engine PR #211
|
|
43
|
+
* https://github.com/classmethod-internal/cortex-engine/pull/211)。#211 以前に
|
|
44
|
+
* NFD で入ったコミットが履歴に残っているので、照合の手前で揃える。
|
|
36
45
|
*/
|
|
37
46
|
function latestGeneratedPaths(cwd) {
|
|
38
47
|
const empty = new Set();
|
|
39
48
|
try {
|
|
40
49
|
// 夜間パイプラインのコミットを新しい順に数件。--grep はコミットメッセージの規約に依存するため、
|
|
41
50
|
// 変わっても「ラベルが出ない」だけで済むように例外は握る。
|
|
42
|
-
const log = (0, node_child_process_1.
|
|
51
|
+
const log = (0, node_child_process_1.execFileSync)("git", ["log", "-n", "20", "--grep=自動追記", "--pretty=format:%H %cI", "--", "Cortex/"], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
43
52
|
if (!log)
|
|
44
53
|
return empty;
|
|
45
54
|
const rows = log.split("\n").map((l) => {
|
|
@@ -53,11 +62,15 @@ function latestGeneratedPaths(cwd) {
|
|
|
53
62
|
for (const r of rows) {
|
|
54
63
|
if (r.day !== latestDay)
|
|
55
64
|
break; // 直近の実行日ぶんだけ
|
|
56
|
-
const files = (0, node_child_process_1.
|
|
57
|
-
for (const f of files.split("\
|
|
58
|
-
|
|
65
|
+
const files = (0, node_child_process_1.execFileSync)("git", ["show", "-z", "--no-color", "--diff-filter=A", "--name-only", "--pretty=format:", r.sha], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
66
|
+
for (const f of files.split("\0")) {
|
|
67
|
+
// 空の `--pretty=format:` では先頭に改行は出ない(git 2.39 で実測)が、format が
|
|
68
|
+
// 非空だと `<ヘッダ>\n<最初のパス>` と改行で区切られる。将来ヘッダを足しても
|
|
69
|
+
// パスが壊れないよう**先頭の改行だけ**落とす(trim だと名前の前後の空白まで削り、
|
|
70
|
+
// その名前のレコードが照合できなくなる)
|
|
71
|
+
const p = f.replace(/^\n+/, "");
|
|
59
72
|
if (p)
|
|
60
|
-
out.add(p);
|
|
73
|
+
out.add(p.normalize("NFC"));
|
|
61
74
|
}
|
|
62
75
|
}
|
|
63
76
|
return out;
|
package/package.json
CHANGED