issue-map 0.4.0 → 0.5.0
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.ja.md +16 -13
- package/README.md +16 -13
- package/README.zh-CN.md +16 -13
- package/README.zh-TW.md +16 -13
- package/package.json +7 -13
- package/{scripts → src}/issue-map-model.ts +44 -22
- package/{scripts → src}/issue-map-page.ts +62 -38
- package/{scripts → src}/issue-map-serve.ts +17 -2
- package/{scripts → src}/issue-map-view.ts +108 -64
- package/{scripts → src}/issue-map.html +2 -1
- package/{scripts → src}/issue-map.ts +275 -129
- /package/{scripts → src}/issue-map-i18n.ts +0 -0
package/README.ja.md
CHANGED
|
@@ -57,13 +57,13 @@ CLI 側(生成メッセージ、エラー)は英語のみです。
|
|
|
57
57
|
|
|
58
58
|
## 設定
|
|
59
59
|
|
|
60
|
-
すべて既定値があり、何も設定しなくても動きます。既定値は `
|
|
60
|
+
すべて既定値があり、何も設定しなくても動きます。既定値は `src/issue-map.ts` の `CONFIG` に
|
|
61
61
|
あります。
|
|
62
62
|
|
|
63
63
|
| 環境変数 | 既定 | 意味 |
|
|
64
64
|
| -------------------------- | --------------------------------- | ---------------------------------------------------------- |
|
|
65
65
|
| `GH_REPO` | cwd の git から推測 | 別の repo を描くときに設定(`gh` 自身の変数。fork も同様) |
|
|
66
|
-
| `ISSUE_MAP_PARENT_HEADING` |
|
|
66
|
+
| `ISSUE_MAP_PARENT_HEADING` | 未設定(本文は取得しない) | サブチケットが本文で親を指す見出し。設定すると本文を読む |
|
|
67
67
|
| `ISSUE_MAP_LABELS_UNREADY` | `needs-triage,needs-info` | 未評価で、まだ誰にも渡せない |
|
|
68
68
|
| `ISSUE_MAP_LABELS_READY` | `ready-for-agent,ready-for-human` | 評価済み・着手可 |
|
|
69
69
|
| `ISSUE_MAP_LABELS_ACTIVE` | `in-progress` | 誰かが対応中(assignee がなくてもよい) |
|
|
@@ -82,6 +82,9 @@ CLI 側(生成メッセージ、エラー)は英語のみです。
|
|
|
82
82
|
- **コマンド名。** `/implement` と `/triage` は向こうの [`implement`](https://github.com/mattpocock/skills/tree/main/skills/engineering/implement) と
|
|
83
83
|
[`triage`](https://github.com/mattpocock/skills/tree/main/skills/engineering/triage) skill です。対象 repo に実在するものを指すようにしてください。さもないと、
|
|
84
84
|
存在しないものを走らせろとマップが指示します。
|
|
85
|
+
- **本文の慣習はオプトイン。** 本文は GitHub のレスポンスの 9 割以上を占めるのに、正規表現
|
|
86
|
+
ひとつを通すだけで、ページの他のどこでも使われません。そのため `ISSUE_MAP_PARENT_HEADING`
|
|
87
|
+
を設定したときだけ本文を取得します。ネイティブ sub-issue は設定不要で、常に優先されます。
|
|
85
88
|
- **クローズ済みの兄弟にはネイティブ sub-issue が要る。** マップが問い合わせるクローズ済みチケット
|
|
86
89
|
は、open なチケットがまだ指しているものだけで、子チケットはネイティブの sub-issue 関係から取り
|
|
87
90
|
ます。`## Parent` の本文慣例を使う repo ではグループ内の**クローズ済み**の子が出てこず、進捗が
|
|
@@ -95,15 +98,15 @@ CLI 側(生成メッセージ、エラー)は英語のみです。
|
|
|
95
98
|
|
|
96
99
|
## ファイル
|
|
97
100
|
|
|
98
|
-
| ファイル
|
|
99
|
-
|
|
|
100
|
-
| `
|
|
101
|
-
| `
|
|
102
|
-
| `
|
|
103
|
-
| `
|
|
104
|
-
| `
|
|
105
|
-
| `
|
|
106
|
-
| `
|
|
101
|
+
| ファイル | 責務 |
|
|
102
|
+
| ------------------------ | ---------------------------------------------------------------------- |
|
|
103
|
+
| `src/issue-map.ts` | スナップショット取得、状態と次の一手の導出、HTML 生成。設定は `CONFIG` |
|
|
104
|
+
| `src/issue-map-model.ts` | 純粋なデータモデル:グルーピング、クリティカルパス、レイアウト |
|
|
105
|
+
| `src/issue-map-i18n.ts` | 4 言語の文言と引き当て |
|
|
106
|
+
| `src/issue-map-page.ts` | ブラウザ側のコード。ビルド時に HTML へバンドルされる |
|
|
107
|
+
| `src/issue-map.html` | テンプレート。2 つのプレースホルダが埋められる |
|
|
108
|
+
| `src/issue-map-serve.ts` | ローカル server。リクエストごとに取り直す |
|
|
109
|
+
| `tools/mutate.ts` | ミューテーションテスト:1 行を壊してテストが赤くなるか見る |
|
|
107
110
|
|
|
108
111
|
## この repo で開発する
|
|
109
112
|
|
|
@@ -123,10 +126,10 @@ bun test # 純粋なモデル層
|
|
|
123
126
|
製品コードに戻し、赤くなることを確かめます:
|
|
124
127
|
|
|
125
128
|
```bash
|
|
126
|
-
bun run mutate
|
|
129
|
+
bun run mutate src/issue-map-model.ts tests/issue-map-layout.test.ts
|
|
127
130
|
```
|
|
128
131
|
|
|
129
|
-
**`
|
|
132
|
+
**`src/issue-map-i18n.ts` を触るとき。** `EN` が原稿であり、キーの定義場所です。3 つの翻訳の型
|
|
130
133
|
はそこから導かれるので、キーや `{n}` の差し込み名が 1 つ欠けると `bun run typecheck` が赤くなります。
|
|
131
134
|
英語で単複を分けるキーは `{ one, other }`、中国語と日本語は 1 本の文字列で構いません。モデル側は文
|
|
132
135
|
を組み立てません——`nextStep` は `{ kind: 'waitChildren', count: 2 }` のような構造化された値で、
|
package/README.md
CHANGED
|
@@ -56,12 +56,12 @@ CLI output (build messages, errors) is English only.
|
|
|
56
56
|
## Configuration
|
|
57
57
|
|
|
58
58
|
Everything has a default — it runs with nothing set. The defaults live in `CONFIG` in
|
|
59
|
-
`
|
|
59
|
+
`src/issue-map.ts`.
|
|
60
60
|
|
|
61
61
|
| Environment variable | Default | Meaning |
|
|
62
62
|
| -------------------------- | --------------------------------- | ------------------------------------------------------------------------------ |
|
|
63
63
|
| `GH_REPO` | inferred from the git in cwd | Map another repo (`gh`'s own variable; forks and multiple remotes are its job) |
|
|
64
|
-
| `ISSUE_MAP_PARENT_HEADING` |
|
|
64
|
+
| `ISSUE_MAP_PARENT_HEADING` | unset (bodies are not fetched) | Body heading under which a sub-issue points at its parent. Set it to opt in |
|
|
65
65
|
| `ISSUE_MAP_LABELS_UNREADY` | `needs-triage,needs-info` | Not assessed yet, cannot be handed to anyone |
|
|
66
66
|
| `ISSUE_MAP_LABELS_READY` | `ready-for-agent,ready-for-human` | Assessed and ready to work on |
|
|
67
67
|
| `ISSUE_MAP_LABELS_ACTIVE` | `in-progress` | Somebody is on it, with or without an assignee |
|
|
@@ -82,6 +82,9 @@ Three worth knowing:
|
|
|
82
82
|
[`implement`](https://github.com/mattpocock/skills/tree/main/skills/engineering/implement) and
|
|
83
83
|
[`triage`](https://github.com/mattpocock/skills/tree/main/skills/engineering/triage) skills. Point
|
|
84
84
|
them at something that exists in the target repo, or the map tells people to run what isn't there.
|
|
85
|
+
- **The body convention is opt-in.** Issue bodies are over 90% of what the GitHub response
|
|
86
|
+
weighs, and they feed one regex — nothing else on the page uses them. So they are only fetched
|
|
87
|
+
when `ISSUE_MAP_PARENT_HEADING` is set. Native sub-issues need no setting and always win.
|
|
85
88
|
- **Closed siblings need native sub-issues.** The map asks GitHub only for the closed issues an
|
|
86
89
|
open one still points at, and children come from the native sub-issue relation. With the
|
|
87
90
|
`## Parent` body convention a group's _closed_ children never appear, so its progress looks
|
|
@@ -95,15 +98,15 @@ tree.
|
|
|
95
98
|
|
|
96
99
|
## Files
|
|
97
100
|
|
|
98
|
-
| File
|
|
99
|
-
|
|
|
100
|
-
| `
|
|
101
|
-
| `
|
|
102
|
-
| `
|
|
103
|
-
| `
|
|
104
|
-
| `
|
|
105
|
-
| `
|
|
106
|
-
| `
|
|
101
|
+
| File | Responsibility |
|
|
102
|
+
| ------------------------ | ------------------------------------------------------------------------ |
|
|
103
|
+
| `src/issue-map.ts` | Snapshot, status and next step, produces the HTML. Knobs in its `CONFIG` |
|
|
104
|
+
| `src/issue-map-model.ts` | Pure data model: grouping, critical path, layout. Shared by both sides |
|
|
105
|
+
| `src/issue-map-i18n.ts` | Strings for the four languages, plus the lookup |
|
|
106
|
+
| `src/issue-map-page.ts` | Browser-side code, bundled into the HTML at build time |
|
|
107
|
+
| `src/issue-map.html` | The template. Two placeholder blocks get filled in |
|
|
108
|
+
| `src/issue-map-serve.ts` | Local server, re-fetches on every request |
|
|
109
|
+
| `tools/mutate.ts` | Mutation testing: break one line, see whether a test goes red |
|
|
107
110
|
|
|
108
111
|
## Developing in this repo
|
|
109
112
|
|
|
@@ -123,10 +126,10 @@ shape, spacing) are deliberately not asserted. A new guard test has to be verifi
|
|
|
123
126
|
the defect it claims to catch back into the product code and confirm it goes red:
|
|
124
127
|
|
|
125
128
|
```bash
|
|
126
|
-
bun run mutate
|
|
129
|
+
bun run mutate src/issue-map-model.ts tests/issue-map-layout.test.ts
|
|
127
130
|
```
|
|
128
131
|
|
|
129
|
-
**Adding to `
|
|
132
|
+
**Adding to `src/issue-map-i18n.ts`.** `EN` is the original and defines the keys; the three
|
|
130
133
|
translations are typed from it, so a missing key or a missing `{n}` placeholder turns
|
|
131
134
|
`bun run typecheck` red. Keys needing English plurals are written `{ one, other }`; Chinese and
|
|
132
135
|
Japanese take a single string. The model never builds sentences — `nextStep` is a structured value
|
package/README.zh-CN.md
CHANGED
|
@@ -54,12 +54,12 @@ CLI 那一侧(产文件消息、错误)只有英文。
|
|
|
54
54
|
|
|
55
55
|
## 配置
|
|
56
56
|
|
|
57
|
-
全部有默认值,一个都不设也跑得起来。默认值长在 `
|
|
57
|
+
全部有默认值,一个都不设也跑得起来。默认值长在 `src/issue-map.ts` 的 `CONFIG`。
|
|
58
58
|
|
|
59
59
|
| 环境变量 | 默认 | 意思 |
|
|
60
60
|
| -------------------------- | --------------------------------- | ------------------------------------------------------ |
|
|
61
61
|
| `GH_REPO` | 从 cwd 的 git 推断 | 要画别的 repo 时设它(`gh` 自己的变量,fork 也交给它) |
|
|
62
|
-
| `ISSUE_MAP_PARENT_HEADING` |
|
|
62
|
+
| `ISSUE_MAP_PARENT_HEADING` | 未设(不抓正文) | 子票在正文指向母票的段落标题。设了才会读正文 |
|
|
63
63
|
| `ISSUE_MAP_LABELS_UNREADY` | `needs-triage,needs-info` | 还没评估完,不能交给谁做 |
|
|
64
64
|
| `ISSUE_MAP_LABELS_READY` | `ready-for-agent,ready-for-human` | 评估完、可以动工 |
|
|
65
65
|
| `ISSUE_MAP_LABELS_ACTIVE` | `in-progress` | 有人在做,不必有 assignee |
|
|
@@ -77,6 +77,9 @@ CLI 那一侧(产文件消息、错误)只有英文。
|
|
|
77
77
|
做」这个标签。)
|
|
78
78
|
- **指令名。** `/implement`、`/triage` 就是那边的 [`implement`](https://github.com/mattpocock/skills/tree/main/skills/engineering/implement) 与
|
|
79
79
|
[`triage`](https://github.com/mattpocock/skills/tree/main/skills/engineering/triage) skill。要指向目标 repo 真的有的东西,不然图上会叫人跑不存在的。
|
|
80
|
+
- **正文惯例要明讲才生效。** 正文占了 GitHub 响应九成以上的重量,而它只喂一条 regex,页面上
|
|
81
|
+
没有别的地方用得到。所以只有设了 `ISSUE_MAP_PARENT_HEADING` 才会去抓正文。原生 sub-issue
|
|
82
|
+
不必设置,而且一律优先。
|
|
80
83
|
- **已完成的兄弟票要靠原生 sub-issue。** 地图只跟 GitHub 要 open 票还牵着的 closed 票,而子票是从
|
|
81
84
|
原生的 sub-issue 关系拿的。用 `## Parent` 正文惯例的 repo 看不到一组里**已完成**的子票,那一组的
|
|
82
85
|
进度会比实际少。把子票在票页的 Sub-issues 关联上去一次就会回来;正文惯例可以留着,原生的本来
|
|
@@ -88,15 +91,15 @@ CLI 那一侧(产文件消息、错误)只有英文。
|
|
|
88
91
|
|
|
89
92
|
## 文件
|
|
90
93
|
|
|
91
|
-
| 文件
|
|
92
|
-
|
|
|
93
|
-
| `
|
|
94
|
-
| `
|
|
95
|
-
| `
|
|
96
|
-
| `
|
|
97
|
-
| `
|
|
98
|
-
| `
|
|
99
|
-
| `
|
|
94
|
+
| 文件 | 职责 |
|
|
95
|
+
| ------------------------ | -------------------------------------------------------- |
|
|
96
|
+
| `src/issue-map.ts` | 抓快照、算状态与下一步、产出 HTML。配置在里面的 `CONFIG` |
|
|
97
|
+
| `src/issue-map-model.ts` | 纯数据模型:分组、关键路径、排版。前后端共用 |
|
|
98
|
+
| `src/issue-map-i18n.ts` | 四种语言的文案与查表 |
|
|
99
|
+
| `src/issue-map-page.ts` | 浏览器端代码,构建时被打包进 HTML |
|
|
100
|
+
| `src/issue-map.html` | 模板。两个占位区块会被填入 |
|
|
101
|
+
| `src/issue-map-serve.ts` | 本机 server,每个请求重抓一次 |
|
|
102
|
+
| `tools/mutate.ts` | 变异测试:改坏一行看测试会不会红 |
|
|
100
103
|
|
|
101
104
|
## 在这个 repo 里开发
|
|
102
105
|
|
|
@@ -114,10 +117,10 @@ bun test # 纯模型那一层
|
|
|
114
117
|
要走反向验证——把它宣称要挡的缺陷放回产品代码,确认它会红:
|
|
115
118
|
|
|
116
119
|
```bash
|
|
117
|
-
bun run mutate
|
|
120
|
+
bun run mutate src/issue-map-model.ts tests/issue-map-layout.test.ts
|
|
118
121
|
```
|
|
119
122
|
|
|
120
|
-
**要动 `
|
|
123
|
+
**要动 `src/issue-map-i18n.ts`。** `EN` 是原稿,也是键的定义处;三份翻译的类型由它推导,少一个
|
|
121
124
|
键或少一个 `{n}` 代入名,`bun run typecheck` 就会红。英文要分单复数的键写成 `{ one, other }`,中日
|
|
122
125
|
文写一句字符串就好。模型那一侧不算句子——`nextStep` 是 `{ kind: 'waitChildren', count: 2 }` 这种结构
|
|
123
126
|
化的值,话在这里才组出来。
|
package/README.zh-TW.md
CHANGED
|
@@ -54,12 +54,12 @@ CLI 那一側(產檔訊息、錯誤)只有英文。
|
|
|
54
54
|
|
|
55
55
|
## 設定
|
|
56
56
|
|
|
57
|
-
全部有預設值,一個都不設也跑得起來。預設值長在 `
|
|
57
|
+
全部有預設值,一個都不設也跑得起來。預設值長在 `src/issue-map.ts` 的 `CONFIG`。
|
|
58
58
|
|
|
59
59
|
| 環境變數 | 預設 | 意思 |
|
|
60
60
|
| -------------------------- | --------------------------------- | ------------------------------------------------------ |
|
|
61
61
|
| `GH_REPO` | 從 cwd 的 git 推斷 | 要畫別的 repo 時設它(`gh` 自己的變數,fork 也交給它) |
|
|
62
|
-
| `ISSUE_MAP_PARENT_HEADING` |
|
|
62
|
+
| `ISSUE_MAP_PARENT_HEADING` | 未設(不抓內文) | 子票在內文指向母票的段落標題。設了才會讀內文 |
|
|
63
63
|
| `ISSUE_MAP_LABELS_UNREADY` | `needs-triage,needs-info` | 還沒評估完,不能交給誰做 |
|
|
64
64
|
| `ISSUE_MAP_LABELS_READY` | `ready-for-agent,ready-for-human` | 評估完、可以動工 |
|
|
65
65
|
| `ISSUE_MAP_LABELS_ACTIVE` | `in-progress` | 有人在做,不必有 assignee |
|
|
@@ -77,6 +77,9 @@ CLI 那一側(產檔訊息、錯誤)只有英文。
|
|
|
77
77
|
做」這個標籤。)
|
|
78
78
|
- **指令名。** `/implement`、`/triage` 就是那邊的 [`implement`](https://github.com/mattpocock/skills/tree/main/skills/engineering/implement) 與
|
|
79
79
|
[`triage`](https://github.com/mattpocock/skills/tree/main/skills/engineering/triage) skill。要指向目標 repo 真的有的東西,不然圖上會叫人跑不存在的。
|
|
80
|
+
- **內文慣例要明講才生效。** 內文佔了 GitHub 回應九成以上的重量,而它只餵一條 regex,頁面上
|
|
81
|
+
沒有別的地方用得到。所以只有設了 `ISSUE_MAP_PARENT_HEADING` 才會去抓內文。原生 sub-issue
|
|
82
|
+
不必設定,而且一律優先。
|
|
80
83
|
- **已完成的兄弟票要靠原生 sub-issue。** 地圖只跟 GitHub 要 open 票還牽著的 closed 票,而子票是從
|
|
81
84
|
原生的 sub-issue 關係拿的。用 `## Parent` 內文慣例的 repo 看不到一組裡**已完成**的子票,那一組的
|
|
82
85
|
進度會比實際少。把子票在票頁的 Sub-issues 關聯上去一次就會回來;內文慣例可以留著,原生的本來
|
|
@@ -88,15 +91,15 @@ CLI 那一側(產檔訊息、錯誤)只有英文。
|
|
|
88
91
|
|
|
89
92
|
## 檔案
|
|
90
93
|
|
|
91
|
-
| 檔案
|
|
92
|
-
|
|
|
93
|
-
| `
|
|
94
|
-
| `
|
|
95
|
-
| `
|
|
96
|
-
| `
|
|
97
|
-
| `
|
|
98
|
-
| `
|
|
99
|
-
| `
|
|
94
|
+
| 檔案 | 責任 |
|
|
95
|
+
| ------------------------ | -------------------------------------------------------- |
|
|
96
|
+
| `src/issue-map.ts` | 抓快照、算狀態與下一步、產出 HTML。設定在裡面的 `CONFIG` |
|
|
97
|
+
| `src/issue-map-model.ts` | 純資料模型:分組、關鍵路徑、排版。前後端共用 |
|
|
98
|
+
| `src/issue-map-i18n.ts` | 四種語言的文案與查表 |
|
|
99
|
+
| `src/issue-map-page.ts` | 瀏覽器端程式碼,建置時被打包進 HTML |
|
|
100
|
+
| `src/issue-map.html` | 樣板。兩個佔位區塊會被填入 |
|
|
101
|
+
| `src/issue-map-serve.ts` | 本機 server,每個請求重抓一次 |
|
|
102
|
+
| `tools/mutate.ts` | 突變測試:改壞一行看測試會不會紅 |
|
|
100
103
|
|
|
101
104
|
## 在這個 repo 裡開發
|
|
102
105
|
|
|
@@ -114,10 +117,10 @@ bun test # 純模型那一層
|
|
|
114
117
|
要走反向驗證——把它宣稱要擋的缺陷放回產品碼,確認它會紅:
|
|
115
118
|
|
|
116
119
|
```bash
|
|
117
|
-
bun run mutate
|
|
120
|
+
bun run mutate src/issue-map-model.ts tests/issue-map-layout.test.ts
|
|
118
121
|
```
|
|
119
122
|
|
|
120
|
-
**要動 `
|
|
123
|
+
**要動 `src/issue-map-i18n.ts`。** `EN` 是原稿,也是鍵的定義處;三份翻譯的型別由它推導,少一個
|
|
121
124
|
鍵或少一個 `{n}` 代入名,`bun run typecheck` 就會紅。英文要分單複數的鍵寫成 `{ one, other }`,中日
|
|
122
125
|
文寫一句字串就好。模型那一側不算句子——`nextStep` 是 `{ kind: 'waitChildren', count: 2 }` 這種結構
|
|
123
126
|
化的值,話在這裡才組出來。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "issue-map",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Draws the blocking relationships between your GitHub Issues as a one-page dev map: which issues can be picked up now, which are waiting on what, and where the critical path runs.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bun",
|
|
@@ -15,27 +15,21 @@
|
|
|
15
15
|
"url": "git+https://github.com/gunter1020/issue-map.git"
|
|
16
16
|
},
|
|
17
17
|
"bin": {
|
|
18
|
-
"issue-map": "./
|
|
19
|
-
"issue-map-build": "./
|
|
18
|
+
"issue-map": "./src/issue-map-serve.ts",
|
|
19
|
+
"issue-map-build": "./src/issue-map.ts"
|
|
20
20
|
},
|
|
21
21
|
"files": [
|
|
22
22
|
"README.zh-TW.md",
|
|
23
23
|
"README.zh-CN.md",
|
|
24
24
|
"README.ja.md",
|
|
25
|
-
"
|
|
26
|
-
"scripts/issue-map-i18n.ts",
|
|
27
|
-
"scripts/issue-map-model.ts",
|
|
28
|
-
"scripts/issue-map-page.ts",
|
|
29
|
-
"scripts/issue-map-view.ts",
|
|
30
|
-
"scripts/issue-map-serve.ts",
|
|
31
|
-
"scripts/issue-map.html"
|
|
25
|
+
"src"
|
|
32
26
|
],
|
|
33
27
|
"type": "module",
|
|
34
28
|
"scripts": {
|
|
35
|
-
"issue-map": "bun run
|
|
36
|
-
"issue-map:serve": "ISSUE_MAP_OPEN=0 bun --watch
|
|
29
|
+
"issue-map": "bun run src/issue-map.ts",
|
|
30
|
+
"issue-map:serve": "ISSUE_MAP_OPEN=0 bun --watch src/issue-map-serve.ts",
|
|
37
31
|
"typecheck": "tsc --noEmit",
|
|
38
|
-
"mutate": "bun run
|
|
32
|
+
"mutate": "bun run tools/mutate.ts",
|
|
39
33
|
"format": "oxfmt .",
|
|
40
34
|
"format:check": "oxfmt --check .",
|
|
41
35
|
"lint": "oxlint .",
|
|
@@ -72,14 +72,36 @@ export type Group = {
|
|
|
72
72
|
export type Snapshot = {
|
|
73
73
|
readonly generatedAt: string
|
|
74
74
|
readonly repo: string
|
|
75
|
-
/**
|
|
76
|
-
|
|
75
|
+
/**
|
|
76
|
+
* 這一次實際生效的標籤字彙與閘門。圖例照它寫,不然改了設定圖例就會說謊。
|
|
77
|
+
*
|
|
78
|
+
* `gated` 是**狀態機當下真的有沒有把這些標籤當閘門**。字彙本身一律有預設值、永遠非空,拿
|
|
79
|
+
* 它的長度去推閘門開著沒有的話,repo 還沒導入標籤時票會判成可動,頁尾卻說沒掛標籤等於未定案。
|
|
80
|
+
*/
|
|
81
|
+
readonly labels: {
|
|
82
|
+
readonly ready: readonly string[]
|
|
83
|
+
readonly unready: readonly string[]
|
|
84
|
+
readonly gated: boolean
|
|
85
|
+
}
|
|
77
86
|
readonly groups: readonly Group[]
|
|
78
87
|
/** 最長的一條依序未完成鏈,也就是最少要幾輪。 */
|
|
79
88
|
readonly criticalPath: number
|
|
80
89
|
readonly issues: readonly MapIssue[]
|
|
81
90
|
}
|
|
82
91
|
|
|
92
|
+
/**
|
|
93
|
+
* 什麼都還沒有的快照。畫面那一側解析行內 JSON 時用它補齊缺的欄位——**缺欄位只在那一個邊界
|
|
94
|
+
* 成立**,收在這裡的話下游拿到的一律是完整的 `Snapshot`,不必每個欄位各自決定「缺了算什麼」。
|
|
95
|
+
*/
|
|
96
|
+
export const EMPTY_SNAPSHOT: Snapshot = {
|
|
97
|
+
generatedAt: '',
|
|
98
|
+
repo: '',
|
|
99
|
+
labels: { ready: [], unready: [], gated: false },
|
|
100
|
+
groups: [],
|
|
101
|
+
criticalPath: 0,
|
|
102
|
+
issues: [],
|
|
103
|
+
}
|
|
104
|
+
|
|
83
105
|
/**
|
|
84
106
|
* 分群:同一張主票底下的子票一群;沒有主票但跟別人有前置關係的合成一群;完全孤立的合成一群。
|
|
85
107
|
* 一張圖畫一群。
|
|
@@ -173,10 +195,8 @@ export type Edge = { readonly from: number; readonly to: number }
|
|
|
173
195
|
export type Layout = {
|
|
174
196
|
/** 每一條線由前到後的站。只有一站的線不標線名。 */
|
|
175
197
|
readonly tracks: readonly (readonly number[])[]
|
|
176
|
-
/**
|
|
198
|
+
/** 孤立的票排成幾列月台。月台接在最後一條線下面,起點就是 `tracks.length`。 */
|
|
177
199
|
readonly islandRows: number
|
|
178
|
-
/** 月台從第幾列開始。 */
|
|
179
|
-
readonly islandFrom: number
|
|
180
200
|
readonly xy: ReadonlyMap<number, Point>
|
|
181
201
|
readonly edges: readonly Edge[]
|
|
182
202
|
readonly width: number
|
|
@@ -190,6 +210,21 @@ const ISLAND_MIN_PER_ROW = 4
|
|
|
190
210
|
*/
|
|
191
211
|
const ISLAND_MAX_PER_ROW = 12
|
|
192
212
|
|
|
213
|
+
/**
|
|
214
|
+
* 一組票內部的阻擋邊。**什麼算一條邊只有這一份定義**——排版照它畫線,頁面也照它判斷這一組畫
|
|
215
|
+
* 出來到底有沒有線;各算一次的話會出現「說要畫圖、畫出來卻沒有線」。
|
|
216
|
+
*/
|
|
217
|
+
export function edgesWithin(members: readonly MapIssue[]): Edge[] {
|
|
218
|
+
const inGroup = new Set(members.map((m) => m.number))
|
|
219
|
+
const edges: Edge[] = []
|
|
220
|
+
for (const m of members) {
|
|
221
|
+
for (const from of m.blockedBy) {
|
|
222
|
+
if (inGroup.has(from)) edges.push({ from, to: m.number })
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return edges
|
|
226
|
+
}
|
|
227
|
+
|
|
193
228
|
/**
|
|
194
229
|
* 把一組票排成線路圖。
|
|
195
230
|
*
|
|
@@ -201,19 +236,12 @@ const ISLAND_MAX_PER_ROW = 12
|
|
|
201
236
|
* 會變成十二條單站線。
|
|
202
237
|
*/
|
|
203
238
|
export function layoutOf(members: readonly MapIssue[]): Layout {
|
|
204
|
-
const
|
|
205
|
-
const preds = new Map(
|
|
206
|
-
|
|
207
|
-
)
|
|
239
|
+
const edges = edgesWithin(members)
|
|
240
|
+
const preds = new Map<number, number[]>(members.map((m) => [m.number, []]))
|
|
241
|
+
for (const edge of edges) preds.get(edge.to)?.push(edge.from)
|
|
208
242
|
const predsOf = (n: number): readonly number[] => preds.get(n) ?? []
|
|
209
243
|
|
|
210
|
-
const hasEdge = new Set
|
|
211
|
-
for (const m of members) {
|
|
212
|
-
for (const from of predsOf(m.number)) {
|
|
213
|
-
hasEdge.add(m.number)
|
|
214
|
-
hasEdge.add(from)
|
|
215
|
-
}
|
|
216
|
-
}
|
|
244
|
+
const hasEdge = new Set(edges.flatMap((edge) => [edge.from, edge.to]))
|
|
217
245
|
const wired = members.filter((m) => hasEdge.has(m.number))
|
|
218
246
|
const island = members.filter((m) => !hasEdge.has(m.number))
|
|
219
247
|
|
|
@@ -257,15 +285,9 @@ export function layoutOf(members: readonly MapIssue[]): Layout {
|
|
|
257
285
|
})
|
|
258
286
|
const islandRows = Math.ceil(island.length / perRow)
|
|
259
287
|
|
|
260
|
-
const edges: Edge[] = []
|
|
261
|
-
for (const m of members) {
|
|
262
|
-
for (const from of predsOf(m.number)) edges.push({ from, to: m.number })
|
|
263
|
-
}
|
|
264
|
-
|
|
265
288
|
return {
|
|
266
289
|
tracks,
|
|
267
290
|
islandRows,
|
|
268
|
-
islandFrom: tracks.length,
|
|
269
291
|
xy,
|
|
270
292
|
edges,
|
|
271
293
|
width: MAP.gutter + perRow * MAP.step + MAP.rightPad,
|
|
@@ -8,18 +8,9 @@
|
|
|
8
8
|
* 這裡剩下的是 DOM 與事件:讀寫 localStorage 的兩個偏好(語言、收合)、事件代理、選取狀態。
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
type Locale,
|
|
15
|
-
LOCALE_NAME,
|
|
16
|
-
LOCALES,
|
|
17
|
-
locale,
|
|
18
|
-
setLocale,
|
|
19
|
-
t,
|
|
20
|
-
} from './issue-map-i18n.ts'
|
|
21
|
-
import { type Snapshot } from './issue-map-model.ts'
|
|
22
|
-
import { esc, type Filter, viewOf } from './issue-map-view.ts'
|
|
11
|
+
import { DEFAULT_LOCALE, isLocale, type Locale, locale, setLocale, t } from './issue-map-i18n.ts'
|
|
12
|
+
import { EMPTY_SNAPSHOT, type Snapshot } from './issue-map-model.ts'
|
|
13
|
+
import { type Filter, langOptionsHTML, viewOf } from './issue-map-view.ts'
|
|
23
14
|
|
|
24
15
|
/** 樣板保證這些節點存在。找不到就是樣板被改壞了,早點喊比畫出半張圖好。 */
|
|
25
16
|
function pick(id: string): HTMLElement {
|
|
@@ -28,9 +19,18 @@ function pick(id: string): HTMLElement {
|
|
|
28
19
|
return node
|
|
29
20
|
}
|
|
30
21
|
|
|
31
|
-
|
|
22
|
+
/**
|
|
23
|
+
* 行內 JSON 是整份快照唯一可能殘缺的地方(沒有資料、被截斷)。缺的欄位在這裡一次補齊,下游
|
|
24
|
+
* 拿到的一律是完整的 `Snapshot`——不然每個用到快照的地方都要自己決定「缺了算什麼」。
|
|
25
|
+
*/
|
|
26
|
+
function readSnapshot(): Snapshot {
|
|
27
|
+
const parsed = JSON.parse(pick('issue-map-data').textContent || '{}') as Partial<Snapshot>
|
|
28
|
+
return { ...EMPTY_SNAPSHOT, ...parsed }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const snapshot = readSnapshot()
|
|
32
32
|
const view = viewOf(snapshot)
|
|
33
|
-
const repo = snapshot.repo
|
|
33
|
+
const repo = snapshot.repo
|
|
34
34
|
|
|
35
35
|
const detail = pick('detail')
|
|
36
36
|
const rowsEl = pick('rows')
|
|
@@ -40,6 +40,28 @@ const tabsEl = pick('tabs')
|
|
|
40
40
|
let selected: number | null = null
|
|
41
41
|
let filter: Filter = 'all'
|
|
42
42
|
|
|
43
|
+
// ---- 偏好 ----
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 偏好記在瀏覽器。**讀不到、寫不進都不是錯誤**:無痕視窗與擋掉儲存的設定裡 `localStorage` 會
|
|
47
|
+
* 直接丟例外,那時偏好只在這一次有效,畫面照樣是完整的——所以存取一律收在這兩支裡。
|
|
48
|
+
*/
|
|
49
|
+
function readStored(key: string): string | null {
|
|
50
|
+
try {
|
|
51
|
+
return localStorage.getItem(key)
|
|
52
|
+
} catch {
|
|
53
|
+
return null
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function writeStored(key: string, value: string): void {
|
|
58
|
+
try {
|
|
59
|
+
localStorage.setItem(key, value)
|
|
60
|
+
} catch {
|
|
61
|
+
// 存不了就只在這一次有效。
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
43
65
|
// ---- 語言 ----
|
|
44
66
|
|
|
45
67
|
/**
|
|
@@ -51,31 +73,20 @@ let filter: Filter = 'all'
|
|
|
51
73
|
const LOCALE_KEY = 'issue-map:locale'
|
|
52
74
|
|
|
53
75
|
function readLocale(): Locale {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if (isLocale(saved)) return saved
|
|
57
|
-
} catch {
|
|
58
|
-
// 讀不到就用預設,畫面照樣是完整的。
|
|
59
|
-
}
|
|
60
|
-
return DEFAULT_LOCALE
|
|
76
|
+
const saved = readStored(LOCALE_KEY)
|
|
77
|
+
return isLocale(saved) ? saved : DEFAULT_LOCALE
|
|
61
78
|
}
|
|
62
79
|
|
|
63
80
|
/** 語言選單只做一次;換語言是整頁重畫,選單自己不重建,不然焦點會掉。 */
|
|
64
81
|
function mountLangPicker(): void {
|
|
65
82
|
const picker = pick('lang')
|
|
66
83
|
if (!(picker instanceof HTMLSelectElement)) throw new Error('#lang 不是 select')
|
|
67
|
-
picker.innerHTML =
|
|
68
|
-
(option) => `<option value="${option}">${esc(LOCALE_NAME[option])}</option>`,
|
|
69
|
-
).join('')
|
|
84
|
+
picker.innerHTML = langOptionsHTML()
|
|
70
85
|
picker.value = locale()
|
|
71
86
|
picker.addEventListener('change', () => {
|
|
72
87
|
if (!isLocale(picker.value)) return
|
|
73
88
|
setLocale(picker.value)
|
|
74
|
-
|
|
75
|
-
localStorage.setItem(LOCALE_KEY, picker.value)
|
|
76
|
-
} catch {
|
|
77
|
-
// 存不了就只在這一次有效。
|
|
78
|
-
}
|
|
89
|
+
writeStored(LOCALE_KEY, picker.value)
|
|
79
90
|
render()
|
|
80
91
|
})
|
|
81
92
|
}
|
|
@@ -90,7 +101,7 @@ const COLLAPSE_KEY = `issue-map:collapsed:${repo}`
|
|
|
90
101
|
|
|
91
102
|
function readFolded(): Set<string> {
|
|
92
103
|
try {
|
|
93
|
-
return new Set(JSON.parse(
|
|
104
|
+
return new Set(JSON.parse(readStored(COLLAPSE_KEY) || '[]') as string[])
|
|
94
105
|
} catch {
|
|
95
106
|
return new Set()
|
|
96
107
|
}
|
|
@@ -102,14 +113,26 @@ function isFolded(parent: number): boolean {
|
|
|
102
113
|
return folded.has(String(parent))
|
|
103
114
|
}
|
|
104
115
|
|
|
116
|
+
/**
|
|
117
|
+
* 掛在某張票底下的東西該不該藏起來。
|
|
118
|
+
*
|
|
119
|
+
* 看的是**整條祖先鏈**,不只直屬主票——三層的鏈收起最上面那張時,第三層的 `data-parent` 指的
|
|
120
|
+
* 是第二層,只比對直屬的話它會單獨留在畫面上。
|
|
121
|
+
*/
|
|
122
|
+
function foldedAnywhere(parent: number): boolean {
|
|
123
|
+
const seen = new Set<number>()
|
|
124
|
+
for (let at: number | null = parent; at !== null && !seen.has(at);) {
|
|
125
|
+
if (isFolded(at)) return true
|
|
126
|
+
seen.add(at)
|
|
127
|
+
at = view.issueAt(at)?.parent ?? null
|
|
128
|
+
}
|
|
129
|
+
return false
|
|
130
|
+
}
|
|
131
|
+
|
|
105
132
|
function setFolded(parent: number, shut: boolean): void {
|
|
106
133
|
if (shut) folded.add(String(parent))
|
|
107
134
|
else folded.delete(String(parent))
|
|
108
|
-
|
|
109
|
-
localStorage.setItem(COLLAPSE_KEY, JSON.stringify([...folded]))
|
|
110
|
-
} catch {
|
|
111
|
-
// 存不了就只在這一次有效,畫面照樣能開合。
|
|
112
|
-
}
|
|
135
|
+
writeStored(COLLAPSE_KEY, JSON.stringify([...folded]))
|
|
113
136
|
paintFolded()
|
|
114
137
|
}
|
|
115
138
|
|
|
@@ -119,7 +142,7 @@ function setFolded(parent: number, shut: boolean): void {
|
|
|
119
142
|
*/
|
|
120
143
|
function paintFolded(): void {
|
|
121
144
|
for (const node of document.querySelectorAll<HTMLElement>('[data-parent]')) {
|
|
122
|
-
node.hidden =
|
|
145
|
+
node.hidden = foldedAnywhere(Number(node.dataset.parent))
|
|
123
146
|
}
|
|
124
147
|
for (const section of document.querySelectorAll<HTMLElement>('section.group[data-fold]')) {
|
|
125
148
|
section.dataset.folded = String(isFolded(Number(section.dataset.fold)))
|
|
@@ -370,10 +393,11 @@ function render(): void {
|
|
|
370
393
|
pick('lede').innerHTML = view.lede()
|
|
371
394
|
pick('stats').innerHTML = view.statsHTML()
|
|
372
395
|
|
|
396
|
+
// 三段都是逃脫過的 HTML,跟建置時填進樣板的是同一批字串——用 textContent 塞會把逃脫顯示出來。
|
|
373
397
|
const foot = view.footerHTML()
|
|
374
|
-
pick('foot-truth').
|
|
398
|
+
pick('foot-truth').innerHTML = foot.truth
|
|
375
399
|
pick('foot-refresh').innerHTML = foot.refresh
|
|
376
|
-
pick('foot-config').
|
|
400
|
+
pick('foot-config').innerHTML = foot.config
|
|
377
401
|
|
|
378
402
|
groupsEl.innerHTML = view.groupsHTML()
|
|
379
403
|
tabsEl.setAttribute('aria-label', t('tabs.aria'))
|
|
@@ -43,19 +43,34 @@ function openInBrowser(url: string): void {
|
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* 同時抵達的請求共用同一次抓取。
|
|
48
|
+
*
|
|
49
|
+
* 一次重新整理按兩下、或開著兩個分頁,本來會各自跑一趟完整的 GitHub 抓取;它們要的是同一刻的
|
|
50
|
+
* 狀態,讓後到的等前一趟就好。抓完就清掉,所以「每次重新整理都是最新的」沒有變。
|
|
51
|
+
*/
|
|
52
|
+
let inFlight: Promise<string> | null = null
|
|
53
|
+
|
|
46
54
|
async function page(): Promise<string> {
|
|
47
|
-
const snapshot = takeSnapshot()
|
|
55
|
+
const snapshot = await takeSnapshot()
|
|
48
56
|
console.log(describe(snapshot))
|
|
49
57
|
return renderDocument(snapshot)
|
|
50
58
|
}
|
|
51
59
|
|
|
60
|
+
function pageShared(): Promise<string> {
|
|
61
|
+
inFlight ??= page().finally(() => {
|
|
62
|
+
inFlight = null
|
|
63
|
+
})
|
|
64
|
+
return inFlight
|
|
65
|
+
}
|
|
66
|
+
|
|
52
67
|
const server = Bun.serve({
|
|
53
68
|
port: PORT,
|
|
54
69
|
async fetch(request) {
|
|
55
70
|
const { pathname } = new URL(request.url)
|
|
56
71
|
if (pathname !== '/') return new Response(null, { status: 404 })
|
|
57
72
|
try {
|
|
58
|
-
return new Response(await
|
|
73
|
+
return new Response(await pageShared(), {
|
|
59
74
|
headers: { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' },
|
|
60
75
|
})
|
|
61
76
|
} catch (error) {
|