issue-map 0.3.0 → 0.3.1
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 +26 -22
- package/README.md +27 -23
- package/README.zh-CN.md +17 -19
- package/README.zh-TW.md +17 -17
- package/package.json +1 -1
- package/scripts/issue-map-serve.ts +2 -2
- package/scripts/issue-map.ts +177 -54
package/README.ja.md
CHANGED
|
@@ -74,8 +74,9 @@ bunx -p issue-map@latest issue-map-build out.html
|
|
|
74
74
|
i18n の層です。スナップショットに文を保存していたら、言語を切り替えるたびに GitHub を取り直す
|
|
75
75
|
ことになります。
|
|
76
76
|
|
|
77
|
-
CLI
|
|
78
|
-
|
|
77
|
+
CLI 側(生成メッセージ、エラー)は英語のみで、ページの言語設定には従いません。あれは
|
|
78
|
+
`bunx issue-map` の出力であり、コマンドを打った人が読むもので、ページの一部ではありません。
|
|
79
|
+
例外は `scripts/mutate.ts` で、この repo 内部のツールなので中国語のままです。
|
|
79
80
|
|
|
80
81
|
## 前提条件
|
|
81
82
|
|
|
@@ -90,36 +91,39 @@ CLI 側(生成メッセージ、エラー)は意図的に中国語のまま
|
|
|
90
91
|
すべて既定値があり、何も設定しなくても動きます。既定値は `scripts/issue-map.ts` の `CONFIG` に
|
|
91
92
|
あります。
|
|
92
93
|
|
|
93
|
-
| 環境変数 | 既定 | 意味
|
|
94
|
-
| -------------------------- | --------------------------------- |
|
|
95
|
-
| `GH_REPO` | cwd の git から推測 | 別の repo を描くときに設定(`gh` 自身の変数。fork や複数 remote の判断も任せます)
|
|
96
|
-
| `ISSUE_MAP_PARENT_HEADING` | `Parent` | サブチケットが本文で親を指す見出し。GitHub ネイティブの sub-issue
|
|
97
|
-
| `ISSUE_MAP_LABELS_UNREADY` | `needs-triage,needs-info` | 付いていれば未評価で、まだ誰にも渡せない
|
|
98
|
-
| `ISSUE_MAP_LABELS_READY` | `ready-for-agent,ready-for-human` | 付いて初めて評価済み・着手可とみなす
|
|
99
|
-
| `ISSUE_MAP_LABELS_ACTIVE` | `in-progress` | 付いていれば誰かが対応中(assignee がなくてもよい)
|
|
100
|
-
| `ISSUE_MAP_LABELS_HUMAN` | `ready-for-human` | 人がやるもの。次の一手に実装コマンドを出さない
|
|
101
|
-
| `ISSUE_MAP_CMD_IMPLEMENT` | `/implement` | 着手できるときにマップが促すコマンド
|
|
102
|
-
| `ISSUE_MAP_CMD_TRIAGE` | `/triage` | まだ評価が要るときにマップが促すコマンド
|
|
103
|
-
| `ISSUE_MAP_PORT` | `4747` | server のポート
|
|
104
|
-
| `ISSUE_MAP_OPEN` | 有効 | `0` にするとブラウザを開かない(`bun --watch` の開発モードは既定で無効)
|
|
105
|
-
|
|
106
|
-
とくに考えておくべきものが
|
|
94
|
+
| 環境変数 | 既定 | 意味 |
|
|
95
|
+
| -------------------------- | --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
96
|
+
| `GH_REPO` | cwd の git から推測 | 別の repo を描くときに設定(`gh` 自身の変数。fork や複数 remote の判断も任せます) |
|
|
97
|
+
| `ISSUE_MAP_PARENT_HEADING` | `Parent` | サブチケットが本文で親を指す見出し。GitHub ネイティブの sub-issue があればそちらを優先。クローズ済みの兄弟が見えるのはネイティブの場合だけ(下記) |
|
|
98
|
+
| `ISSUE_MAP_LABELS_UNREADY` | `needs-triage,needs-info` | 付いていれば未評価で、まだ誰にも渡せない |
|
|
99
|
+
| `ISSUE_MAP_LABELS_READY` | `ready-for-agent,ready-for-human` | 付いて初めて評価済み・着手可とみなす |
|
|
100
|
+
| `ISSUE_MAP_LABELS_ACTIVE` | `in-progress` | 付いていれば誰かが対応中(assignee がなくてもよい) |
|
|
101
|
+
| `ISSUE_MAP_LABELS_HUMAN` | `ready-for-human` | 人がやるもの。次の一手に実装コマンドを出さない |
|
|
102
|
+
| `ISSUE_MAP_CMD_IMPLEMENT` | `/implement` | 着手できるときにマップが促すコマンド |
|
|
103
|
+
| `ISSUE_MAP_CMD_TRIAGE` | `/triage` | まだ評価が要るときにマップが促すコマンド |
|
|
104
|
+
| `ISSUE_MAP_PORT` | `4747` | server のポート |
|
|
105
|
+
| `ISSUE_MAP_OPEN` | 有効 | `0` にするとブラウザを開かない(`bun --watch` の開発モードは既定で無効) |
|
|
106
|
+
|
|
107
|
+
とくに考えておくべきものが 3 つ:
|
|
107
108
|
|
|
108
109
|
- **ラベルの語彙**:対象 repo がこのセットを使っていないなら、その repo の名前に置き換えてください。
|
|
109
110
|
コード側で検知します——スナップショットに ready/unready のどのラベルも現れないときは triage を
|
|
110
111
|
ゲートにしません。そうしないと全チケットが「未評価」になってしまいます。
|
|
111
112
|
- **コマンド名**:`/implement` と `/triage` は Claude Code の skill です。対象 repo にそれがない
|
|
112
113
|
場合は必ず差し替えてください。さもないと、存在しないものを走らせろとマップが指示します。
|
|
114
|
+
- **クローズ済みの兄弟にはネイティブ sub-issue が要る**:マップが GitHub に問い合わせるクローズ済み
|
|
115
|
+
チケットは、open なチケットがまだ指しているものだけです——そのブロッカー、その親、そして親の
|
|
116
|
+
子チケット。子チケットは GitHub ネイティブの sub-issue 関係から取るので、`## Parent` の本文慣例を
|
|
117
|
+
使う repo ではグループ内の**クローズ済み**の子が出てこず、進捗が実際より少なく見えます。open な
|
|
118
|
+
チケットは影響を受けません。代わりに repo のクローズ済みチケットを全部走査する手もありますが、
|
|
119
|
+
古い repo ではわずか数枚のために数十回のリクエストを払うことになります。ネイティブ sub-issue より
|
|
120
|
+
前に作られたグループなら、子チケットを一度リンクし直す(チケットの Sub-issues)だけでクローズ済みの
|
|
121
|
+
子が戻ります。本文の慣例は残しておいて構いません——ネイティブのほうが優先されます。
|
|
113
122
|
|
|
114
123
|
## よくある失敗
|
|
115
124
|
|
|
116
|
-
- `gh api graphql
|
|
125
|
+
- `gh api graphql failed: …` — `gh` にログインしていないか、cwd が対象 repo の git ツリーの中に
|
|
117
126
|
ありません。
|
|
118
|
-
- `open issue 超過 100 張,這支要改成分頁抓` — GraphQL の `first` の上限が 100 です。それ以上の
|
|
119
|
-
チケットに対応するには `issue-map.ts` の `query()` にページングを足す必要があります。これは設定
|
|
120
|
-
ではなくコードの変更です。
|
|
121
|
-
|
|
122
|
-
(CLI のメッセージは中国語なので、実際に出力される通りに引用しています。)
|
|
123
127
|
|
|
124
128
|
## ファイル
|
|
125
129
|
|
package/README.md
CHANGED
|
@@ -71,8 +71,9 @@ The model and fetch side **no longer builds sentences**: `nextStep` is a structu
|
|
|
71
71
|
`{ kind: 'waitChildren', count: 2 }`, and so are group names. The words are assembled in the i18n
|
|
72
72
|
layer. If the snapshot stored sentences, switching language would mean re-fetching from GitHub.
|
|
73
73
|
|
|
74
|
-
The CLI side (build messages, errors) is
|
|
75
|
-
|
|
74
|
+
The CLI side (build messages, errors) is English only and does not follow the page's language
|
|
75
|
+
setting: it is the output of `bunx issue-map`, read by whoever ran the command, not part of the
|
|
76
|
+
page. `scripts/mutate.ts` is the exception — it is an in-repo tool, so it stays Chinese.
|
|
76
77
|
|
|
77
78
|
## Requirements
|
|
78
79
|
|
|
@@ -87,20 +88,20 @@ develops this tool, not part of the page.
|
|
|
87
88
|
Everything has a default — it runs with nothing set. The defaults live in `CONFIG` in
|
|
88
89
|
`scripts/issue-map.ts`.
|
|
89
90
|
|
|
90
|
-
| Environment variable | Default | Meaning
|
|
91
|
-
| -------------------------- | --------------------------------- |
|
|
92
|
-
| `GH_REPO` | inferred from the git in cwd | Set it to map another repo (`gh`'s own variable; forks and multiple remotes are its job too)
|
|
93
|
-
| `ISSUE_MAP_PARENT_HEADING` | `Parent` | The body heading under which a sub-issue points at its parent. A native GitHub sub-issue relation wins when present |
|
|
94
|
-
| `ISSUE_MAP_LABELS_UNREADY` | `needs-triage,needs-info` | Carrying one means it is not assessed yet and cannot be handed to anyone
|
|
95
|
-
| `ISSUE_MAP_LABELS_READY` | `ready-for-agent,ready-for-human` | Only with one of these does an issue count as assessed and ready to work on
|
|
96
|
-
| `ISSUE_MAP_LABELS_ACTIVE` | `in-progress` | Carrying one means somebody is on it, with or without an assignee
|
|
97
|
-
| `ISSUE_MAP_LABELS_HUMAN` | `ready-for-human` | These need a person, so the next step is not an implementation command
|
|
98
|
-
| `ISSUE_MAP_CMD_IMPLEMENT` | `/implement` | The command the map tells you to run when an issue is ready
|
|
99
|
-
| `ISSUE_MAP_CMD_TRIAGE` | `/triage` | The command the map tells you to run when it still needs assessing
|
|
100
|
-
| `ISSUE_MAP_PORT` | `4747` | Port for the server
|
|
101
|
-
| `ISSUE_MAP_OPEN` | on | Set `0` to stop opening the browser (the `bun --watch` dev mode has it off)
|
|
102
|
-
|
|
103
|
-
|
|
91
|
+
| Environment variable | Default | Meaning |
|
|
92
|
+
| -------------------------- | --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
93
|
+
| `GH_REPO` | inferred from the git in cwd | Set it to map another repo (`gh`'s own variable; forks and multiple remotes are its job too) |
|
|
94
|
+
| `ISSUE_MAP_PARENT_HEADING` | `Parent` | The body heading under which a sub-issue points at its parent. A native GitHub sub-issue relation wins when present, and only native ones show _closed_ siblings (see below) |
|
|
95
|
+
| `ISSUE_MAP_LABELS_UNREADY` | `needs-triage,needs-info` | Carrying one means it is not assessed yet and cannot be handed to anyone |
|
|
96
|
+
| `ISSUE_MAP_LABELS_READY` | `ready-for-agent,ready-for-human` | Only with one of these does an issue count as assessed and ready to work on |
|
|
97
|
+
| `ISSUE_MAP_LABELS_ACTIVE` | `in-progress` | Carrying one means somebody is on it, with or without an assignee |
|
|
98
|
+
| `ISSUE_MAP_LABELS_HUMAN` | `ready-for-human` | These need a person, so the next step is not an implementation command |
|
|
99
|
+
| `ISSUE_MAP_CMD_IMPLEMENT` | `/implement` | The command the map tells you to run when an issue is ready |
|
|
100
|
+
| `ISSUE_MAP_CMD_TRIAGE` | `/triage` | The command the map tells you to run when it still needs assessing |
|
|
101
|
+
| `ISSUE_MAP_PORT` | `4747` | Port for the server |
|
|
102
|
+
| `ISSUE_MAP_OPEN` | on | Set `0` to stop opening the browser (the `bun --watch` dev mode has it off) |
|
|
103
|
+
|
|
104
|
+
Three worth thinking through:
|
|
104
105
|
|
|
105
106
|
- **Label vocabulary**: if the target repo does not use this set, replace them with its own names.
|
|
106
107
|
The code detects it — when the snapshot contains none of the ready/unready labels at all, triage
|
|
@@ -108,16 +109,19 @@ Two worth thinking through:
|
|
|
108
109
|
- **Command names**: `/implement` and `/triage` are Claude Code skills. If the target repo has no
|
|
109
110
|
such skills you must change them, or the map will tell people to run something that does not
|
|
110
111
|
exist.
|
|
112
|
+
- **Closed siblings need native sub-issues**: the map asks GitHub only for the closed issues an
|
|
113
|
+
open one still points at — its blockers, its parent, and that parent's children. Children come
|
|
114
|
+
from GitHub's native sub-issue relation, so with the `## Parent` body convention a group's
|
|
115
|
+
_closed_ children never appear and its progress looks smaller than it is. Open issues are not
|
|
116
|
+
affected. The alternative is scanning every closed issue in the repo, which on an old repo means
|
|
117
|
+
dozens of requests to find a handful of issues. If a group predates native sub-issues, linking
|
|
118
|
+
its children once (the issue's Sub-issues panel) brings the closed ones back; the body convention
|
|
119
|
+
can stay, the native relation wins anyway.
|
|
111
120
|
|
|
112
121
|
## When it fails
|
|
113
122
|
|
|
114
|
-
- `gh api graphql
|
|
115
|
-
tree.
|
|
116
|
-
- `open issue 超過 100 張,這支要改成分頁抓` — 100 is the hard cap on GraphQL's `first`. Supporting
|
|
117
|
-
more issues means adding pagination to `query()` in `issue-map.ts`; that is a code change, not
|
|
118
|
-
configuration.
|
|
119
|
-
|
|
120
|
-
(CLI messages are in Chinese, so they are quoted here as they actually appear.)
|
|
123
|
+
- `gh api graphql failed: …` — `gh` is not logged in, or your cwd is not inside the target repo's
|
|
124
|
+
git tree.
|
|
121
125
|
|
|
122
126
|
## Files
|
|
123
127
|
|
package/README.zh-CN.md
CHANGED
|
@@ -61,7 +61,7 @@ bunx -p issue-map@latest issue-map-build out.html
|
|
|
61
61
|
的值,分组名字也一样,话在 i18n 那一层才组出来。快照里存中文句子的话,换一次语言就得重抓一次
|
|
62
62
|
GitHub。
|
|
63
63
|
|
|
64
|
-
CLI
|
|
64
|
+
CLI 那一侧(产文件消息、错误)只有英文,也不跟着页面的语言走:那是 `bunx issue-map` 的输出,给下指令的人看的,不是页面的一部分。例外是 `scripts/mutate.ts`——它是这个 repo 内部的工具,留中文。
|
|
65
65
|
|
|
66
66
|
## 前置条件
|
|
67
67
|
|
|
@@ -74,30 +74,28 @@ CLI 那一侧(产文件消息、错误)刻意留中文:那是给开发者
|
|
|
74
74
|
|
|
75
75
|
全部有默认值,一个都不设也跑得起来。默认值长在 `scripts/issue-map.ts` 的 `CONFIG`。
|
|
76
76
|
|
|
77
|
-
| 环境变量 | 默认 | 意思
|
|
78
|
-
| -------------------------- | --------------------------------- |
|
|
79
|
-
| `GH_REPO` | 从 cwd 的 git 推断 | 要画别的 repo 时设它(`gh` 自己的变量,fork 与多 remote 也交给它)
|
|
80
|
-
| `ISSUE_MAP_PARENT_HEADING` | `Parent` | 子票在正文指向母票的段落标题。GitHub 原生 sub-issue
|
|
81
|
-
| `ISSUE_MAP_LABELS_UNREADY` | `needs-triage,needs-info` | 挂了就是还没评估完,不能交给谁做
|
|
82
|
-
| `ISSUE_MAP_LABELS_READY` | `ready-for-agent,ready-for-human` | 挂了才算评估完、可以动工
|
|
83
|
-
| `ISSUE_MAP_LABELS_ACTIVE` | `in-progress` | 挂了代表有人在做,不必有 assignee
|
|
84
|
-
| `ISSUE_MAP_LABELS_HUMAN` | `ready-for-human` | 这些要人做,下一步不写实作指令
|
|
85
|
-
| `ISSUE_MAP_CMD_IMPLEMENT` | `/implement` | 可以动工时图上叫人跑的指令
|
|
86
|
-
| `ISSUE_MAP_CMD_TRIAGE` | `/triage` | 还要评估时图上叫人跑的指令
|
|
87
|
-
| `ISSUE_MAP_PORT` | `4747` | server 的端口
|
|
88
|
-
| `ISSUE_MAP_OPEN` | 开 | 设 `0` 就不自动开浏览器(`bun --watch` 的开发模式默认关掉)
|
|
89
|
-
|
|
90
|
-
|
|
77
|
+
| 环境变量 | 默认 | 意思 |
|
|
78
|
+
| -------------------------- | --------------------------------- | ---------------------------------------------------------------------------------------------------------- |
|
|
79
|
+
| `GH_REPO` | 从 cwd 的 git 推断 | 要画别的 repo 时设它(`gh` 自己的变量,fork 与多 remote 也交给它) |
|
|
80
|
+
| `ISSUE_MAP_PARENT_HEADING` | `Parent` | 子票在正文指向母票的段落标题。GitHub 原生 sub-issue 有值时优先,而且只有原生的看得到已完成的兄弟票(见下) |
|
|
81
|
+
| `ISSUE_MAP_LABELS_UNREADY` | `needs-triage,needs-info` | 挂了就是还没评估完,不能交给谁做 |
|
|
82
|
+
| `ISSUE_MAP_LABELS_READY` | `ready-for-agent,ready-for-human` | 挂了才算评估完、可以动工 |
|
|
83
|
+
| `ISSUE_MAP_LABELS_ACTIVE` | `in-progress` | 挂了代表有人在做,不必有 assignee |
|
|
84
|
+
| `ISSUE_MAP_LABELS_HUMAN` | `ready-for-human` | 这些要人做,下一步不写实作指令 |
|
|
85
|
+
| `ISSUE_MAP_CMD_IMPLEMENT` | `/implement` | 可以动工时图上叫人跑的指令 |
|
|
86
|
+
| `ISSUE_MAP_CMD_TRIAGE` | `/triage` | 还要评估时图上叫人跑的指令 |
|
|
87
|
+
| `ISSUE_MAP_PORT` | `4747` | server 的端口 |
|
|
88
|
+
| `ISSUE_MAP_OPEN` | 开 | 设 `0` 就不自动开浏览器(`bun --watch` 的开发模式默认关掉) |
|
|
89
|
+
|
|
90
|
+
三个要特别想过的:
|
|
91
91
|
|
|
92
92
|
- **标签词汇**:目标 repo 没在用这套标签就要换成它自己的名字。程序会侦测——快照里完全没出现 ready/unready 任何一个标签时,就不拿 triage 当闸门,否则每张票都会变成「待评估」。
|
|
93
93
|
- **指令名**:`/implement`、`/triage` 是 Claude Code 的 skill。目标 repo 没有的话一定要换掉,不然图上会叫人跑不存在的东西。
|
|
94
|
+
- **已完成的兄弟票要靠原生 sub-issue**:地图只跟 GitHub 要 open 票还牵着的 closed 票——它的阻挡者、它的 parent,以及那个 parent 底下的子票。子票是从 GitHub 原生的 sub-issue 关系拿的,所以用 `## Parent` 正文惯例的 repo 看不到一组里**已完成**的子票,那一组的进度会比实际少。open 票不受影响。另一条路是整包扫过 repo 里所有 closed 票,而那在老 repo 上是几十次请求换个位数张票。一组票如果是原生 sub-issue 之前开的,把子票关联上去一次(票页的 Sub-issues)就会回来;正文惯例可以留着,原生的本来就优先。
|
|
94
95
|
|
|
95
96
|
## 常见失败
|
|
96
97
|
|
|
97
|
-
- `gh api graphql
|
|
98
|
-
- `open issue 超過 100 張,這支要改成分頁抓` — GraphQL 的 `first` 上限就是 100。要支持更多票得在 `issue-map.ts` 的 `query()` 加分页;这是要改程序,不是配置。
|
|
99
|
-
|
|
100
|
-
(CLI 消息是繁体中文,所以这里照它实际印出来的样子引用。)
|
|
98
|
+
- `gh api graphql failed: …` — `gh` 没登录,或 cwd 不在目标 repo 的 git 树里。
|
|
101
99
|
|
|
102
100
|
## 文件
|
|
103
101
|
|
package/README.zh-TW.md
CHANGED
|
@@ -61,7 +61,7 @@ bunx -p issue-map@latest issue-map-build out.html
|
|
|
61
61
|
的值,分組名字也一樣,話在 i18n 那一層才組出來。快照裡存中文句子的話,換一次語言就得重抓一次
|
|
62
62
|
GitHub。
|
|
63
63
|
|
|
64
|
-
CLI
|
|
64
|
+
CLI 那一側(產檔訊息、錯誤)只有英文,也不跟著頁面的語言走:那是 `bunx issue-map` 的輸出,給下指令的人看的,不是頁面的一部分。例外是 `scripts/mutate.ts`——它是這個 repo 內部的工具,留中文。
|
|
65
65
|
|
|
66
66
|
## 前置條件
|
|
67
67
|
|
|
@@ -74,28 +74,28 @@ CLI 那一側(產檔訊息、錯誤)刻意留中文:那是給開發者看
|
|
|
74
74
|
|
|
75
75
|
全部有預設值,一個都不設也跑得起來。預設值長在 `scripts/issue-map.ts` 的 `CONFIG`。
|
|
76
76
|
|
|
77
|
-
| 環境變數 | 預設 | 意思
|
|
78
|
-
| -------------------------- | --------------------------------- |
|
|
79
|
-
| `GH_REPO` | 從 cwd 的 git 推斷 | 要畫別的 repo 時設它(`gh` 自己的變數,fork 與多 remote 也交給它)
|
|
80
|
-
| `ISSUE_MAP_PARENT_HEADING` | `Parent` | 子票在內文指向母票的段落標題。GitHub 原生 sub-issue
|
|
81
|
-
| `ISSUE_MAP_LABELS_UNREADY` | `needs-triage,needs-info` | 掛了就是還沒評估完,不能交給誰做
|
|
82
|
-
| `ISSUE_MAP_LABELS_READY` | `ready-for-agent,ready-for-human` | 掛了才算評估完、可以動工
|
|
83
|
-
| `ISSUE_MAP_LABELS_ACTIVE` | `in-progress` | 掛了代表有人在做,不必有 assignee
|
|
84
|
-
| `ISSUE_MAP_LABELS_HUMAN` | `ready-for-human` | 這些要人做,下一步不寫實作指令
|
|
85
|
-
| `ISSUE_MAP_CMD_IMPLEMENT` | `/implement` | 可以動工時圖上叫人跑的指令
|
|
86
|
-
| `ISSUE_MAP_CMD_TRIAGE` | `/triage` | 還要評估時圖上叫人跑的指令
|
|
87
|
-
| `ISSUE_MAP_PORT` | `4747` | server 的 port
|
|
88
|
-
| `ISSUE_MAP_OPEN` | 開 | 設 `0` 就不自動開瀏覽器(`bun --watch` 的開發模式預設關掉)
|
|
89
|
-
|
|
90
|
-
|
|
77
|
+
| 環境變數 | 預設 | 意思 |
|
|
78
|
+
| -------------------------- | --------------------------------- | ---------------------------------------------------------------------------------------------------------- |
|
|
79
|
+
| `GH_REPO` | 從 cwd 的 git 推斷 | 要畫別的 repo 時設它(`gh` 自己的變數,fork 與多 remote 也交給它) |
|
|
80
|
+
| `ISSUE_MAP_PARENT_HEADING` | `Parent` | 子票在內文指向母票的段落標題。GitHub 原生 sub-issue 有值時優先,而且只有原生的看得到已完成的兄弟票(見下) |
|
|
81
|
+
| `ISSUE_MAP_LABELS_UNREADY` | `needs-triage,needs-info` | 掛了就是還沒評估完,不能交給誰做 |
|
|
82
|
+
| `ISSUE_MAP_LABELS_READY` | `ready-for-agent,ready-for-human` | 掛了才算評估完、可以動工 |
|
|
83
|
+
| `ISSUE_MAP_LABELS_ACTIVE` | `in-progress` | 掛了代表有人在做,不必有 assignee |
|
|
84
|
+
| `ISSUE_MAP_LABELS_HUMAN` | `ready-for-human` | 這些要人做,下一步不寫實作指令 |
|
|
85
|
+
| `ISSUE_MAP_CMD_IMPLEMENT` | `/implement` | 可以動工時圖上叫人跑的指令 |
|
|
86
|
+
| `ISSUE_MAP_CMD_TRIAGE` | `/triage` | 還要評估時圖上叫人跑的指令 |
|
|
87
|
+
| `ISSUE_MAP_PORT` | `4747` | server 的 port |
|
|
88
|
+
| `ISSUE_MAP_OPEN` | 開 | 設 `0` 就不自動開瀏覽器(`bun --watch` 的開發模式預設關掉) |
|
|
89
|
+
|
|
90
|
+
三個要特別想過的:
|
|
91
91
|
|
|
92
92
|
- **標籤字彙**:目標 repo 沒在用這套標籤就要換成它自己的名字。程式會偵測——快照裡完全沒出現 ready/unready 任何一個標籤時,就不拿 triage 當閘門,否則每張票都會變成「待評估」。
|
|
93
93
|
- **指令名**:`/implement`、`/triage` 是 Claude Code 的 skill。目標 repo 沒有的話一定要換掉,不然圖上會叫人跑不存在的東西。
|
|
94
|
+
- **已完成的兄弟票要靠原生 sub-issue**:地圖只跟 GitHub 要 open 票還牽著的 closed 票——它的阻擋者、它的 parent,以及那個 parent 底下的子票。子票是從 GitHub 原生的 sub-issue 關係拿的,所以用 `## Parent` 內文慣例的 repo 看不到一組裡**已完成**的子票,那一組的進度會比實際少。open 票不受影響。另一條路是整包掃過 repo 裡所有 closed 票,而那在老 repo 上是幾十次請求換個位數張票。一組票如果是原生 sub-issue 之前開的,把子票關聯上去一次(票頁的 Sub-issues)就會回來;內文慣例可以留著,原生的本來就優先。
|
|
94
95
|
|
|
95
96
|
## 常見失敗
|
|
96
97
|
|
|
97
|
-
- `gh api graphql
|
|
98
|
-
- `open issue 超過 100 張,這支要改成分頁抓` — GraphQL 的 `first` 上限就是 100。要支援更多票得在 `issue-map.ts` 的 `query()` 加分頁;這是要改程式,不是設定。
|
|
98
|
+
- `gh api graphql failed: …` — `gh` 沒登入,或 cwd 不在目標 repo 的 git 樹裡。
|
|
99
99
|
|
|
100
100
|
## 檔案
|
|
101
101
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "issue-map",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
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",
|
|
@@ -36,7 +36,7 @@ function openInBrowser(url: string): void {
|
|
|
36
36
|
const result = spawnSync(command, { stdout: 'ignore', stderr: 'pipe' })
|
|
37
37
|
if (!result.success) {
|
|
38
38
|
console.error(
|
|
39
|
-
|
|
39
|
+
`Could not open the browser (${command[0]}: ${result.stderr.toString().trim()}) — open the URL above yourself`,
|
|
40
40
|
)
|
|
41
41
|
}
|
|
42
42
|
}
|
|
@@ -72,5 +72,5 @@ const server = Bun.serve({
|
|
|
72
72
|
})
|
|
73
73
|
|
|
74
74
|
const url = `http://localhost:${server.port}`
|
|
75
|
-
console.log(
|
|
75
|
+
console.log(`Dev map: ${url} (every refresh re-fetches from GitHub)`)
|
|
76
76
|
if (OPEN) openInBrowser(url)
|
package/scripts/issue-map.ts
CHANGED
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
* 帶進快照的 issue:所有 open issue,加上仍被 open issue 牽著的 closed issue。後者畫成「已完成」
|
|
12
12
|
* 的節點讓進度看得見,沒人牽著之後自然消失。
|
|
13
13
|
*
|
|
14
|
+
* closed 是**指名**去要的(阻擋者、parent、parent 底下的子票),不掃整包——老 repo 幾千張
|
|
15
|
+
* closed 裡通常只有個位數會留下。代價:同一組裡已完成的兄弟票要靠 GitHub 原生 sub-issue 才
|
|
16
|
+
* 抽得到,用內文 `## Parent` 慣例的 repo 看不到它們,那一組的進度會比實際少。
|
|
17
|
+
*
|
|
14
18
|
* **要畫哪個 repo**:從 cwd 的 git 推斷,不必填——在那個 repo 裡跑 `bunx issue-map@latest`
|
|
15
19
|
* 就好。要指定別的 repo 設 `GH_REPO`。標籤字彙與 parent 的慣例都能用環境變數調,見底下的
|
|
16
20
|
* `CONFIG`,整份對照表在 README。
|
|
@@ -42,8 +46,10 @@ const TEMPLATE = new URL('./issue-map.html', import.meta.url).pathname
|
|
|
42
46
|
const CLIENT = new URL('./issue-map-page.ts', import.meta.url).pathname
|
|
43
47
|
const OUTPUT = process.argv[2] ?? 'dist/issue-map.html'
|
|
44
48
|
|
|
45
|
-
/**
|
|
49
|
+
/** 一頁的張數。GraphQL 的 `first` 最多就是 100,票再多就靠 cursor 一頁一頁接。 */
|
|
46
50
|
const PAGE = 100
|
|
51
|
+
/** 一次用 alias 指名幾張票。GraphQL 對單一查詢的節點數有上限,這個量級離它還很遠。 */
|
|
52
|
+
const BATCH = 50
|
|
47
53
|
|
|
48
54
|
function labelList(raw: string | undefined, fallback: string): readonly string[] {
|
|
49
55
|
return (raw ?? fallback)
|
|
@@ -96,50 +102,163 @@ type RawIssue = {
|
|
|
96
102
|
/** 一路帶著算好的 parent,免得同一段內文被 regex 掃好幾次。 */
|
|
97
103
|
type Issue = RawIssue & { readonly parentNumber: number | null }
|
|
98
104
|
|
|
99
|
-
interface
|
|
100
|
-
pageInfo: { hasNextPage: boolean }
|
|
101
|
-
nodes:
|
|
105
|
+
interface Page<T> {
|
|
106
|
+
pageInfo: { hasNextPage: boolean; endCursor: string | null }
|
|
107
|
+
nodes: T[]
|
|
102
108
|
}
|
|
103
|
-
|
|
104
|
-
|
|
109
|
+
|
|
110
|
+
/** 票號查不到(號碼其實是 PR,或那張票不存在)時 GraphQL 回 null。 */
|
|
111
|
+
type MaybeIssue = RawIssue | null
|
|
112
|
+
|
|
113
|
+
const ISSUE_FIELDS = `
|
|
114
|
+
number title state url body closedAt
|
|
115
|
+
author { login }
|
|
116
|
+
parent { number }
|
|
117
|
+
labels(first: 20) { nodes { name } }
|
|
118
|
+
assignees(first: 10) { nodes { login } }
|
|
119
|
+
blockedBy(first: 50) { nodes { number } }
|
|
120
|
+
`
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 跑一次 `gh api graphql`。
|
|
124
|
+
*
|
|
125
|
+
* 票號是我們自己從前一次結果拿到的整數,直接組進查詢字串;只有 cursor 走變數——它是 API 給的
|
|
126
|
+
* 不透明字串,沒有理由自己去逃脫它。
|
|
127
|
+
*/
|
|
128
|
+
function run<T>(query: string, variables: Record<string, string> = {}): T {
|
|
129
|
+
// `{owner}`/`{repo}` 由 gh 從 cwd 的 git 推斷,`GH_REPO` 可以蓋過去。
|
|
130
|
+
const args = [
|
|
131
|
+
'gh',
|
|
132
|
+
'api',
|
|
133
|
+
'graphql',
|
|
134
|
+
'-f',
|
|
135
|
+
`query=${query}`,
|
|
136
|
+
'-F',
|
|
137
|
+
'owner={owner}',
|
|
138
|
+
'-F',
|
|
139
|
+
'repo={repo}',
|
|
140
|
+
]
|
|
141
|
+
for (const [name, value] of Object.entries(variables)) args.push('-f', `${name}=${value}`)
|
|
142
|
+
const result = spawnSync(args, { stdout: 'pipe', stderr: 'pipe' })
|
|
143
|
+
if (result.exitCode !== 0) throw new Error(`gh api graphql failed: ${result.stderr.toString()}`)
|
|
144
|
+
const parsed = JSON.parse(result.stdout.toString()) as { data: T; errors?: unknown }
|
|
145
|
+
if (parsed.errors) throw new Error(`GraphQL errors: ${JSON.stringify(parsed.errors)}`)
|
|
146
|
+
return parsed.data
|
|
105
147
|
}
|
|
106
148
|
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
149
|
+
const OPEN_QUERY = `
|
|
150
|
+
query($owner: String!, $repo: String!, $after: String) {
|
|
151
|
+
repository(owner: $owner, name: $repo) {
|
|
152
|
+
nameWithOwner
|
|
153
|
+
issues(states: OPEN, first: ${PAGE}, after: $after, orderBy: { field: CREATED_AT, direction: DESC }) {
|
|
154
|
+
pageInfo { hasNextPage endCursor }
|
|
155
|
+
nodes { ${ISSUE_FIELDS} }
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}`
|
|
159
|
+
|
|
160
|
+
/** open issue 全部都要,所以一路翻到底——票超過一頁不是錯誤,是常態。 */
|
|
161
|
+
function fetchOpen(): { nameWithOwner: string; open: RawIssue[] } {
|
|
162
|
+
type Result = { repository: { nameWithOwner: string; issues: Page<RawIssue> } }
|
|
163
|
+
const open: RawIssue[] = []
|
|
164
|
+
let nameWithOwner = ''
|
|
165
|
+
let after: string | null = null
|
|
166
|
+
for (;;) {
|
|
167
|
+
const variables: Record<string, string> = after ? { after } : {}
|
|
168
|
+
const { repository } = run<Result>(OPEN_QUERY, variables)
|
|
169
|
+
nameWithOwner = repository.nameWithOwner
|
|
170
|
+
open.push(...repository.issues.nodes)
|
|
171
|
+
const { hasNextPage, endCursor } = repository.issues.pageInfo
|
|
172
|
+
if (!hasNextPage || !endCursor) break
|
|
173
|
+
after = endCursor
|
|
116
174
|
}
|
|
117
|
-
|
|
175
|
+
return { nameWithOwner, open }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** alias 不能以數字開頭,所以票號前面補一個 `i`。 */
|
|
179
|
+
const alias = (number: number) => `i${number}`
|
|
118
180
|
|
|
119
|
-
|
|
120
|
-
const
|
|
181
|
+
function chunks<T>(items: readonly T[], size: number): T[][] {
|
|
182
|
+
const batches: T[][] = []
|
|
183
|
+
for (let index = 0; index < items.length; index += size) {
|
|
184
|
+
batches.push(items.slice(index, index + size))
|
|
185
|
+
}
|
|
186
|
+
return batches
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** 指名要哪幾張票,一批 alias 問完。查不到的就當沒有。 */
|
|
190
|
+
function fetchByNumber(numbers: readonly number[]): RawIssue[] {
|
|
191
|
+
type Result = { repository: Record<string, MaybeIssue> }
|
|
192
|
+
const found: RawIssue[] = []
|
|
193
|
+
for (const batch of chunks(numbers, BATCH)) {
|
|
194
|
+
const query = `
|
|
121
195
|
query($owner: String!, $repo: String!) {
|
|
122
196
|
repository(owner: $owner, name: $repo) {
|
|
123
|
-
|
|
124
|
-
open: issues(states: OPEN, first: ${PAGE}, orderBy: { field: CREATED_AT, direction: DESC }) { ${FIELDS} }
|
|
125
|
-
closed: issues(states: CLOSED, first: ${PAGE}, orderBy: { field: CREATED_AT, direction: DESC }) { ${FIELDS} }
|
|
197
|
+
${batch.map((number) => `${alias(number)}: issue(number: ${number}) { ${ISSUE_FIELDS} }`).join('\n ')}
|
|
126
198
|
}
|
|
127
199
|
}`
|
|
200
|
+
const { repository } = run<Result>(query)
|
|
201
|
+
for (const number of batch) {
|
|
202
|
+
const issue = repository[alias(number)]
|
|
203
|
+
if (issue) found.push(issue)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return found
|
|
207
|
+
}
|
|
128
208
|
|
|
129
|
-
function
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
209
|
+
function childrenQuery(parent: number): string {
|
|
210
|
+
return `
|
|
211
|
+
query($owner: String!, $repo: String!, $after: String) {
|
|
212
|
+
repository(owner: $owner, name: $repo) {
|
|
213
|
+
issue(number: ${parent}) {
|
|
214
|
+
subIssues(first: ${PAGE}, after: $after) {
|
|
215
|
+
pageInfo { hasNextPage endCursor }
|
|
216
|
+
nodes { ${ISSUE_FIELDS} }
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}`
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* 拿這些 parent 底下的子票,為的是把同一組裡**已完成**的兄弟票撈出來當進度。
|
|
225
|
+
*
|
|
226
|
+
* 只有 GitHub 原生 sub-issue 有值。用內文 `## Parent` 慣例的 repo 這裡是空的,那些已完成的
|
|
227
|
+
* 兄弟票就不會出現在圖上——要把它們找回來只能整包掃 closed,而那對老 repo 是幾十次請求換
|
|
228
|
+
* 幾張票。open 的兄弟不必靠這裡,它們本來就在 open 那包。
|
|
229
|
+
*/
|
|
230
|
+
function fetchChildren(parents: readonly number[]): RawIssue[] {
|
|
231
|
+
type Batch = { repository: Record<string, { subIssues: Page<RawIssue> } | null> }
|
|
232
|
+
type More = { repository: { issue: { subIssues: Page<RawIssue> } | null } }
|
|
233
|
+
const children: RawIssue[] = []
|
|
234
|
+
for (const batch of chunks(parents, BATCH)) {
|
|
235
|
+
const query = `
|
|
236
|
+
query($owner: String!, $repo: String!) {
|
|
237
|
+
repository(owner: $owner, name: $repo) {
|
|
238
|
+
${batch
|
|
239
|
+
.map(
|
|
240
|
+
(number) => `${alias(number)}: issue(number: ${number}) {
|
|
241
|
+
subIssues(first: ${PAGE}) { pageInfo { hasNextPage endCursor } nodes { ${ISSUE_FIELDS} } } }`,
|
|
242
|
+
)
|
|
243
|
+
.join('\n ')}
|
|
244
|
+
}
|
|
245
|
+
}`
|
|
246
|
+
const { repository } = run<Batch>(query)
|
|
247
|
+
for (const number of batch) {
|
|
248
|
+
const page = repository[alias(number)]?.subIssues
|
|
249
|
+
if (!page) continue
|
|
250
|
+
children.push(...page.nodes)
|
|
251
|
+
// 子票破百的 parent 很罕見,就讓它自己續抓,不為了它把整批都變成分頁查詢。
|
|
252
|
+
let after = page.pageInfo.hasNextPage ? page.pageInfo.endCursor : null
|
|
253
|
+
while (after) {
|
|
254
|
+
const next = run<More>(childrenQuery(number), { after }).repository.issue?.subIssues
|
|
255
|
+
if (!next) break
|
|
256
|
+
children.push(...next.nodes)
|
|
257
|
+
after = next.pageInfo.hasNextPage ? next.pageInfo.endCursor : null
|
|
258
|
+
}
|
|
259
|
+
}
|
|
141
260
|
}
|
|
142
|
-
return
|
|
261
|
+
return children
|
|
143
262
|
}
|
|
144
263
|
|
|
145
264
|
/** 原生 sub-issue 優先;沒有就讀內文的 `## <標題>` 之後第一個 `#<n>`。 */
|
|
@@ -150,9 +269,8 @@ function withParent(raw: RawIssue): Issue {
|
|
|
150
269
|
}
|
|
151
270
|
|
|
152
271
|
export function takeSnapshot(): Snapshot {
|
|
153
|
-
const
|
|
154
|
-
const open =
|
|
155
|
-
const closed = repository.closed.nodes.map(withParent)
|
|
272
|
+
const { nameWithOwner, open: rawOpen } = fetchOpen()
|
|
273
|
+
const open = rawOpen.map(withParent)
|
|
156
274
|
|
|
157
275
|
const openNumbers = new Set(open.map((issue) => issue.number))
|
|
158
276
|
const openParents = new Set(open.map((issue) => issue.parentNumber).filter(isNumber))
|
|
@@ -160,17 +278,17 @@ export function takeSnapshot(): Snapshot {
|
|
|
160
278
|
open.flatMap((issue) => issue.blockedBy.nodes.map((blocker) => blocker.number)),
|
|
161
279
|
)
|
|
162
280
|
|
|
163
|
-
//
|
|
164
|
-
// parent
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
]
|
|
281
|
+
// 進圖的 closed issue 只有 open issue 還牽著的那些:它們的阻擋者、它們的 parent,以及同一個
|
|
282
|
+
// parent 底下已完成的兄弟(那一組的進度)。所以不掃整包 closed,而是指名去要——老 repo 的
|
|
283
|
+
// 幾千張 closed 裡通常只有個位數會留下,翻完它們是拿幾十次請求換幾張票。
|
|
284
|
+
const referenced = [...new Set([...blockers, ...openParents])].filter(
|
|
285
|
+
(number) => !openNumbers.has(number),
|
|
286
|
+
)
|
|
287
|
+
const closed = dedupe([...fetchByNumber(referenced), ...fetchChildren([...openParents])])
|
|
288
|
+
.filter((issue) => issue.state === 'CLOSED' && !openNumbers.has(issue.number))
|
|
289
|
+
.map(withParent)
|
|
290
|
+
|
|
291
|
+
const kept = [...open, ...closed]
|
|
174
292
|
const parents = new Set(kept.map((issue) => issue.parentNumber).filter(isNumber))
|
|
175
293
|
const openChildren = new Map<number, number>()
|
|
176
294
|
for (const issue of kept) {
|
|
@@ -187,7 +305,7 @@ export function takeSnapshot(): Snapshot {
|
|
|
187
305
|
.toSorted((a, b) => a.number - b.number)
|
|
188
306
|
return {
|
|
189
307
|
generatedAt: new Date().toISOString(),
|
|
190
|
-
repo:
|
|
308
|
+
repo: nameWithOwner,
|
|
191
309
|
labels: { ready: CONFIG.ready, unready: CONFIG.unready },
|
|
192
310
|
groups: groupsOf(issues),
|
|
193
311
|
criticalPath: criticalPathOf(issues),
|
|
@@ -195,6 +313,11 @@ export function takeSnapshot(): Snapshot {
|
|
|
195
313
|
}
|
|
196
314
|
}
|
|
197
315
|
|
|
316
|
+
/** 同一張票可能同時是某人的阻擋者又是某人的兄弟,用票號收斂成一張。 */
|
|
317
|
+
function dedupe(issues: readonly RawIssue[]): RawIssue[] {
|
|
318
|
+
return [...new Map(issues.map((issue) => [issue.number, issue])).values()]
|
|
319
|
+
}
|
|
320
|
+
|
|
198
321
|
function isNumber(value: number | null): value is number {
|
|
199
322
|
return value !== null
|
|
200
323
|
}
|
|
@@ -281,9 +404,9 @@ async function bundleClient(): Promise<string> {
|
|
|
281
404
|
format: 'iife',
|
|
282
405
|
minify: false,
|
|
283
406
|
})
|
|
284
|
-
if (!built.success) throw new Error(
|
|
407
|
+
if (!built.success) throw new Error(`Failed to bundle ${CLIENT}: ${built.logs.join('\n')}`)
|
|
285
408
|
const [output] = built.outputs
|
|
286
|
-
if (!output) throw new Error(
|
|
409
|
+
if (!output) throw new Error(`Bundling ${CLIENT} produced no output`)
|
|
287
410
|
return output.text()
|
|
288
411
|
}
|
|
289
412
|
|
|
@@ -307,17 +430,17 @@ export async function renderFragment(snapshot: Snapshot): Promise<string> {
|
|
|
307
430
|
|
|
308
431
|
function replaceIn(html: string, marker: RegExp, body: string, what: string): string {
|
|
309
432
|
const next = html.replace(marker, `$1${body}$2`)
|
|
310
|
-
if (next === html) throw new Error(
|
|
433
|
+
if (next === html) throw new Error(`Template is missing the ${what} block: ${TEMPLATE}`)
|
|
311
434
|
return next
|
|
312
435
|
}
|
|
313
436
|
|
|
314
437
|
export function describe(snapshot: Snapshot): string {
|
|
315
438
|
const done = snapshot.issues.filter((issue) => issue.status === 'done').length
|
|
316
|
-
return `${snapshot.issues.length - done}
|
|
439
|
+
return `${snapshot.issues.length - done} unfinished, ${done} closed but still referenced (${snapshot.generatedAt})`
|
|
317
440
|
}
|
|
318
441
|
|
|
319
442
|
if (import.meta.main) {
|
|
320
443
|
const snapshot = takeSnapshot()
|
|
321
444
|
await Bun.write(OUTPUT, await renderFragment(snapshot))
|
|
322
|
-
console.log(
|
|
445
|
+
console.log(`Wrote ${OUTPUT}: ${describe(snapshot)}`)
|
|
323
446
|
}
|