@bluewombat/manager-github 0.1.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.md +162 -0
- package/SPECS.md +126 -0
- package/dist/adapter/github/fetch-issue.d.ts +29 -0
- package/dist/adapter/github/fetch-issue.d.ts.map +1 -0
- package/dist/adapter/github/fetch-issue.js +34 -0
- package/dist/adapter/github/fetch-issue.js.map +1 -0
- package/dist/adapter/normalize/body.d.ts +24 -0
- package/dist/adapter/normalize/body.d.ts.map +1 -0
- package/dist/adapter/normalize/body.js +104 -0
- package/dist/adapter/normalize/body.js.map +1 -0
- package/dist/adapter/normalize/fingerprint.d.ts +8 -0
- package/dist/adapter/normalize/fingerprint.d.ts.map +1 -0
- package/dist/adapter/normalize/fingerprint.js +26 -0
- package/dist/adapter/normalize/fingerprint.js.map +1 -0
- package/dist/adapter/normalize/intent.d.ts +27 -0
- package/dist/adapter/normalize/intent.d.ts.map +1 -0
- package/dist/adapter/normalize/intent.js +57 -0
- package/dist/adapter/normalize/intent.js.map +1 -0
- package/dist/adapter/normalize/labels.d.ts +17 -0
- package/dist/adapter/normalize/labels.d.ts.map +1 -0
- package/dist/adapter/normalize/labels.js +43 -0
- package/dist/adapter/normalize/labels.js.map +1 -0
- package/dist/adapter/normalize/noise.d.ts +13 -0
- package/dist/adapter/normalize/noise.d.ts.map +1 -0
- package/dist/adapter/normalize/noise.js +28 -0
- package/dist/adapter/normalize/noise.js.map +1 -0
- package/dist/adapter/normalize/normalize.d.ts +22 -0
- package/dist/adapter/normalize/normalize.d.ts.map +1 -0
- package/dist/adapter/normalize/normalize.js +216 -0
- package/dist/adapter/normalize/normalize.js.map +1 -0
- package/dist/adapter/progress/emit.d.ts +3 -0
- package/dist/adapter/progress/emit.d.ts.map +1 -0
- package/dist/adapter/progress/emit.js +6 -0
- package/dist/adapter/progress/emit.js.map +1 -0
- package/dist/adapter/raw/parse-raw.d.ts +14 -0
- package/dist/adapter/raw/parse-raw.d.ts.map +1 -0
- package/dist/adapter/raw/parse-raw.js +47 -0
- package/dist/adapter/raw/parse-raw.js.map +1 -0
- package/dist/adapter/raw/unwrap.d.ts +12 -0
- package/dist/adapter/raw/unwrap.d.ts.map +1 -0
- package/dist/adapter/raw/unwrap.js +19 -0
- package/dist/adapter/raw/unwrap.js.map +1 -0
- package/dist/adapter/run/run-adapter.d.ts +28 -0
- package/dist/adapter/run/run-adapter.d.ts.map +1 -0
- package/dist/adapter/run/run-adapter.js +131 -0
- package/dist/adapter/run/run-adapter.js.map +1 -0
- package/dist/adapter/types.d.ts +69 -0
- package/dist/adapter/types.d.ts.map +1 -0
- package/dist/adapter/types.js +3 -0
- package/dist/adapter/types.js.map +1 -0
- package/dist/emitter/event/field-matrix.d.ts +29 -0
- package/dist/emitter/event/field-matrix.d.ts.map +1 -0
- package/dist/emitter/event/field-matrix.js +94 -0
- package/dist/emitter/event/field-matrix.js.map +1 -0
- package/dist/emitter/github/comments.d.ts +14 -0
- package/dist/emitter/github/comments.d.ts.map +1 -0
- package/dist/emitter/github/comments.js +69 -0
- package/dist/emitter/github/comments.js.map +1 -0
- package/dist/emitter/github/labels.d.ts +18 -0
- package/dist/emitter/github/labels.d.ts.map +1 -0
- package/dist/emitter/github/labels.js +81 -0
- package/dist/emitter/github/labels.js.map +1 -0
- package/dist/emitter/github/result.d.ts +19 -0
- package/dist/emitter/github/result.d.ts.map +1 -0
- package/dist/emitter/github/result.js +2 -0
- package/dist/emitter/github/result.js.map +1 -0
- package/dist/emitter/progress/emit.d.ts +3 -0
- package/dist/emitter/progress/emit.d.ts.map +1 -0
- package/dist/emitter/progress/emit.js +6 -0
- package/dist/emitter/progress/emit.js.map +1 -0
- package/dist/emitter/render/render-event.d.ts +20 -0
- package/dist/emitter/render/render-event.d.ts.map +1 -0
- package/dist/emitter/render/render-event.js +293 -0
- package/dist/emitter/render/render-event.js.map +1 -0
- package/dist/emitter/render/truncate.d.ts +9 -0
- package/dist/emitter/render/truncate.d.ts.map +1 -0
- package/dist/emitter/render/truncate.js +10 -0
- package/dist/emitter/render/truncate.js.map +1 -0
- package/dist/emitter/run/run-emitter.d.ts +27 -0
- package/dist/emitter/run/run-emitter.d.ts.map +1 -0
- package/dist/emitter/run/run-emitter.js +121 -0
- package/dist/emitter/run/run-emitter.js.map +1 -0
- package/dist/emitter/thread/marker.d.ts +15 -0
- package/dist/emitter/thread/marker.d.ts.map +1 -0
- package/dist/emitter/thread/marker.js +37 -0
- package/dist/emitter/thread/marker.js.map +1 -0
- package/dist/emitter/types.d.ts +64 -0
- package/dist/emitter/types.d.ts.map +1 -0
- package/dist/emitter/types.js +3 -0
- package/dist/emitter/types.js.map +1 -0
- package/dist/github/client.d.ts +63 -0
- package/dist/github/client.d.ts.map +1 -0
- package/dist/github/client.js +96 -0
- package/dist/github/client.js.map +1 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +357 -0
- package/dist/index.js.map +1 -0
- package/dist/issue-number.d.ts +5 -0
- package/dist/issue-number.d.ts.map +1 -0
- package/dist/issue-number.js +32 -0
- package/dist/issue-number.js.map +1 -0
- package/dist/labels.d.ts +3 -0
- package/dist/labels.d.ts.map +1 -0
- package/dist/labels.js +24 -0
- package/dist/labels.js.map +1 -0
- package/dist/listener/delivery/run-hook.d.ts +8 -0
- package/dist/listener/delivery/run-hook.d.ts.map +1 -0
- package/dist/listener/delivery/run-hook.js +50 -0
- package/dist/listener/delivery/run-hook.js.map +1 -0
- package/dist/listener/github/list-issues.d.ts +30 -0
- package/dist/listener/github/list-issues.d.ts.map +1 -0
- package/dist/listener/github/list-issues.js +36 -0
- package/dist/listener/github/list-issues.js.map +1 -0
- package/dist/listener/progress/emit.d.ts +3 -0
- package/dist/listener/progress/emit.d.ts.map +1 -0
- package/dist/listener/progress/emit.js +6 -0
- package/dist/listener/progress/emit.js.map +1 -0
- package/dist/listener/run/run-listener.d.ts +35 -0
- package/dist/listener/run/run-listener.d.ts.map +1 -0
- package/dist/listener/run/run-listener.js +217 -0
- package/dist/listener/run/run-listener.js.map +1 -0
- package/dist/listener/source/select-events.d.ts +46 -0
- package/dist/listener/source/select-events.d.ts.map +1 -0
- package/dist/listener/source/select-events.js +102 -0
- package/dist/listener/source/select-events.js.map +1 -0
- package/dist/listener/types.d.ts +56 -0
- package/dist/listener/types.d.ts.map +1 -0
- package/dist/listener/types.js +3 -0
- package/dist/listener/types.js.map +1 -0
- package/dist/manager.d.ts +29 -0
- package/dist/manager.d.ts.map +1 -0
- package/dist/manager.js +256 -0
- package/dist/manager.js.map +1 -0
- package/dist/probe/probe-issue.d.ts +20 -0
- package/dist/probe/probe-issue.d.ts.map +1 -0
- package/dist/probe/probe-issue.js +45 -0
- package/dist/probe/probe-issue.js.map +1 -0
- package/dist/repo.d.ts +6 -0
- package/dist/repo.d.ts.map +1 -0
- package/dist/repo.js +14 -0
- package/dist/repo.js.map +1 -0
- package/dist/setup.d.ts +43 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +281 -0
- package/dist/setup.js.map +1 -0
- package/dist/submission/submission.d.ts +44 -0
- package/dist/submission/submission.d.ts.map +1 -0
- package/dist/submission/submission.js +189 -0
- package/dist/submission/submission.js.map +1 -0
- package/dist/written-fingerprint.d.ts +7 -0
- package/dist/written-fingerprint.d.ts.map +1 -0
- package/dist/written-fingerprint.js +38 -0
- package/dist/written-fingerprint.js.map +1 -0
- package/package.json +51 -0
package/README.md
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# Manager: GitHub Issues
|
|
2
|
+
|
|
3
|
+
The FeatureManager triplet for GitHub Issues: issues in, issue comments (and
|
|
4
|
+
optional state labels) out, over the REST API with a token.
|
|
5
|
+
Contract: [SPECS.md](./SPECS.md).
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @bluewombat/runtime @bluewombat/manager-github
|
|
9
|
+
npx mason init --manager @bluewombat/manager-github --manager-option repo=owner/name
|
|
10
|
+
export GITHUB_TOKEN=…
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```json
|
|
14
|
+
{
|
|
15
|
+
"manager": "@bluewombat/manager-github",
|
|
16
|
+
"managerOptions": {
|
|
17
|
+
"repo": "owner/name",
|
|
18
|
+
"labels": ["mason"],
|
|
19
|
+
"readyLabel": "ready",
|
|
20
|
+
"defaultProject": "app",
|
|
21
|
+
"stateLabelPrefix": "mason:"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The token is read by this package from the environment; it never appears in the
|
|
27
|
+
config file and Host never sees it.
|
|
28
|
+
|
|
29
|
+
## What an issue says
|
|
30
|
+
|
|
31
|
+
The body is the intention, word for word, less what GitHub itself put there:
|
|
32
|
+
the comments an issue template leaves behind, and the boxes of a task list —
|
|
33
|
+
`- [ ] a CSV downloads` reaches the Planner as `- a CSV downloads`. Nothing is
|
|
34
|
+
read out of it: no checklist is required, no criteria are extracted (what
|
|
35
|
+
"done" means is the layer above mason's, not mason's). A body that was only a
|
|
36
|
+
template's comments falls back to the title.
|
|
37
|
+
|
|
38
|
+
That text, with the title, the project and the priority, is the issue's
|
|
39
|
+
fingerprint — what tells an **edit** from an **echo**. Comments, state labels
|
|
40
|
+
and a ticked box all move `updated_at` and deliver the issue again; none of
|
|
41
|
+
them changes the fingerprint. Rewording the body does, and on a Feature in
|
|
42
|
+
flight that is recorded and freezes it once the current unit lands, for a
|
|
43
|
+
human to decide. So: tick the boxes as you please; rewrite the words only when
|
|
44
|
+
you mean it.
|
|
45
|
+
|
|
46
|
+
## Options
|
|
47
|
+
|
|
48
|
+
| Option | Required | Default | Meaning |
|
|
49
|
+
| ------------------ | -------- | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
50
|
+
| `repo` | yes | — | `owner/name` to poll |
|
|
51
|
+
| `tokenEnv` | no | `GITHUB_TOKEN` | Environment variable holding the token |
|
|
52
|
+
| `token` | no | — | The token itself. For embedding and tests; prefer `tokenEnv` |
|
|
53
|
+
| `apiBase` | no | `https://api.github.com` | REST base, for GitHub Enterprise |
|
|
54
|
+
| `labels` | no | none | Every listed label must be on the issue for it to be admitted |
|
|
55
|
+
| `readyLabel` | no | `ready` | On an escalated Feature, this label means resume |
|
|
56
|
+
| `defaultProject` | no | `unknown` | Project when no `project:` label says otherwise |
|
|
57
|
+
| `defaultPriority` | no | `50` | Priority when no `priority:` label says otherwise (0…100) |
|
|
58
|
+
| `stateLabelPrefix` | no | none | When set, state labels (`mason:done`, …) are moved too |
|
|
59
|
+
| `branch` | no | `main` | The work line pull requests are opened onto. `mason setup` checks it exists and names the repository's default branch when it does not |
|
|
60
|
+
| `remote` | no | `https://github.com/<repo>.git` | Git URL of the repository, for Host's copy of the work line. Over HTTPS, git authenticates with the token in `tokenEnv` — read when git asks, never written to disk — so pushes are the token's account. Set it for GitHub Enterprise, a mirror, or SSH (then this machine's keys are used, and `doctor` says so) |
|
|
61
|
+
| `keepBranch` | no | `false` | Leave the feature branch on the remote after the fold. By default it is deleted once the squash is on the work line |
|
|
62
|
+
| `commitAuthor` | no | none | `Name <email>`: who the system is on the commits it makes. Without it, commits carry this machine's git identity, and `doctor` warns. Use the token's account (its noreply address works) so GitHub credits the system, not a person |
|
|
63
|
+
|
|
64
|
+
## Submissions
|
|
65
|
+
|
|
66
|
+
This manager is an Authority: it offers an assembled feature to the repository
|
|
67
|
+
as a pull request, and takes an accepted one in.
|
|
68
|
+
|
|
69
|
+
| Method | On GitHub |
|
|
70
|
+
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
71
|
+
| `submit` | Opens a pull request from the published branch onto `workLine.branch`. An open one on the same branch is reused, never duplicated |
|
|
72
|
+
| `verdict` | Reads the checks on the pull request's head commit |
|
|
73
|
+
| `fold` | Squash-merges it: the commit's title is the pull request's; its body is what the Project's Describer said (carried on the pull request), else `Closes #N` alone. Then deletes the branch unless `keepBranch` |
|
|
74
|
+
|
|
75
|
+
The pull request is titled after the issue (trailing full stop dropped, `(#N)`
|
|
76
|
+
appended) and its body is the issue's intention, the Plan's steps, and
|
|
77
|
+
`Closes #N` — so GitHub closes the issue when the pull request is merged.
|
|
78
|
+
Nothing in it is generated.
|
|
79
|
+
|
|
80
|
+
`verdict` counts the grace for a commit with no check from the **commit**, not
|
|
81
|
+
from the pull request: one that was sent back and published again is old while
|
|
82
|
+
its commit is seconds old, and reading "no check yet" as "no checks exist" folds
|
|
83
|
+
it unjudged.
|
|
84
|
+
|
|
85
|
+
`clearReady` takes the `readyLabel` off an issue once a resume has been acted
|
|
86
|
+
on, so an escalation freezes instead of resuming on every pass.
|
|
87
|
+
|
|
88
|
+
`probe` asks whether an issue Host already holds is still there. A poll cannot
|
|
89
|
+
see a deletion: the issue is simply missing from every later page. Open is
|
|
90
|
+
present; closed or 404 is gone (Host treats that as abandon). Any other error
|
|
91
|
+
is unavailable, so a missed read does not cancel work.
|
|
92
|
+
|
|
93
|
+
`referenceManager` names where the reference work line is — `remote` and
|
|
94
|
+
`branch` — and who the system is there — `credentialEnv` (the token variable,
|
|
95
|
+
for an HTTPS remote) and `author` (from `commitAuthor`) — in the words
|
|
96
|
+
`@bluewombat/isolation-git` reads. With it, Host keeps its own copy at
|
|
97
|
+
`<home>/work-line` (`.mason` by default) and `workLine.stable` need not be set.
|
|
98
|
+
|
|
99
|
+
One rule about identity: the only thing done in a person's name is the issue
|
|
100
|
+
they wrote. Comments, labels, pull requests and merges are the token's
|
|
101
|
+
account; commits are `commitAuthor`; pushes and fetches use the token. A copy
|
|
102
|
+
made before `remote` changed (SSH to HTTPS, say) no longer matches: delete it
|
|
103
|
+
and the next run clones again.
|
|
104
|
+
|
|
105
|
+
## Prepare the repository
|
|
106
|
+
|
|
107
|
+
`mason setup` reads the config and says what the repository still needs;
|
|
108
|
+
`--apply` creates it. It is idempotent, so it is safe to re-run after changing
|
|
109
|
+
`labels` or turning `stateLabelPrefix` on.
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
mason setup --apply
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
| Checked | How |
|
|
116
|
+
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
117
|
+
| The repository is reachable | `GET /repos/{owner}/{repo}` — a 404 means "not found, or this token cannot see it" |
|
|
118
|
+
| Issues are enabled | `has_issues` on that same response |
|
|
119
|
+
| The token can write | `permissions.push` on that same response, rather than guessing at scopes |
|
|
120
|
+
| The branch exists | `GET /repos/{owner}/{repo}/branches/{branch}` for `branch` (default `main`); when it is missing and the config named none, the step says the repository's default branch and what to write |
|
|
121
|
+
| Labels | `labels`, `readyLabel`, `project:<defaultProject>`, and one per Event when `stateLabelPrefix` is set |
|
|
122
|
+
|
|
123
|
+
Creating a label is the only write; nothing existing is renamed, recoloured, or
|
|
124
|
+
deleted.
|
|
125
|
+
|
|
126
|
+
## Stack
|
|
127
|
+
|
|
128
|
+
Repository stack — TypeScript, Node 24.20, Biome, `node:test`. See [docs/DEVELOPMENT.md](../../../docs/DEVELOPMENT.md).
|
|
129
|
+
|
|
130
|
+
## Setup
|
|
131
|
+
|
|
132
|
+
From the repository root:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
nvm use
|
|
136
|
+
npm install
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Commands
|
|
140
|
+
|
|
141
|
+
| Task | Command |
|
|
142
|
+
| --------- | ---------------- |
|
|
143
|
+
| Lint | `npm run lint` |
|
|
144
|
+
| Format | `npm run format` |
|
|
145
|
+
| Typecheck | `npm run tsc` |
|
|
146
|
+
| Test | `npm test` |
|
|
147
|
+
| Build | `npm run build` |
|
|
148
|
+
|
|
149
|
+
## Layout
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
SPECS.md the contract
|
|
153
|
+
README.md how to build and configure it
|
|
154
|
+
src/
|
|
155
|
+
index.ts createManager, checkManager, scaffoldManager — the plugin face
|
|
156
|
+
manager.ts the ManagerPort (listen / adapt / report / probe live in listener/, adapter/, emitter/, probe/)
|
|
157
|
+
github/ Octokit channel shared by listen, adapt, report, and probe
|
|
158
|
+
probe/ whether a minted key's issue is still on the tracker
|
|
159
|
+
repo.ts owner/name
|
|
160
|
+
issue-number.ts the issue a key or a payload points at
|
|
161
|
+
labels.ts whether a payload carries a label
|
|
162
|
+
```
|
package/SPECS.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# Manager: GitHub Issues
|
|
2
|
+
|
|
3
|
+
A plugin, not a Transformer. GitHub Issues in, issue comments (and optional
|
|
4
|
+
state labels) out, as one `ManagerPort`. Listen / adapt / report live in this
|
|
5
|
+
package. GitHub-the-git-host is out of scope — only the tracker.
|
|
6
|
+
|
|
7
|
+
## 1. Job
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
Host manager-github
|
|
11
|
+
---- --------------
|
|
12
|
+
managerOptions read repo / labels / token
|
|
13
|
+
a Cursor poll the issue list
|
|
14
|
+
a key probe: is the issue still there
|
|
15
|
+
one semantic Event comment, and maybe label
|
|
16
|
+
a published branch submit: open (or reuse) the pull request
|
|
17
|
+
a Submission reference fold: squash-merge it, delete the branch unless keepBranch
|
|
18
|
+
← ManagerPort
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
**In:** a `ManagerContext`. **Out:** a `ManagerPort`, or a reason.
|
|
22
|
+
|
|
23
|
+
## 2. The token
|
|
24
|
+
|
|
25
|
+
Read from `context.env[tokenEnv]`, defaulting to `GITHUB_TOKEN`. A `token`
|
|
26
|
+
option exists for embedding and tests. `createManager` refuses with the name of
|
|
27
|
+
the variable it wanted when neither is set — the value is never echoed, logged,
|
|
28
|
+
or written to a Thread.
|
|
29
|
+
|
|
30
|
+
## 3. Keys
|
|
31
|
+
|
|
32
|
+
`github:<owner>/<name>#<issue>`, or `github:unconvertible` when the payload
|
|
33
|
+
carries no issue number. `report` recovers the issue from the key: every key
|
|
34
|
+
this package mints ends in `#<issue>`, so no issue number crosses the Port.
|
|
35
|
+
|
|
36
|
+
## 3b. The intention and its fingerprint
|
|
37
|
+
|
|
38
|
+
The issue body is the intention, verbatim, less what the tracker itself put
|
|
39
|
+
there: the HTML comments an issue template leaves behind, and the boxes of a
|
|
40
|
+
task list (`- [ ]`, `- [x]` — the bullet and the words stay). Line ends are
|
|
41
|
+
normalised, trailing blanks and runs of blank lines collapsed. Nothing is read:
|
|
42
|
+
no criteria, no headings, no meaning. A body that was only that falls back to
|
|
43
|
+
the title, like no body at all.
|
|
44
|
+
|
|
45
|
+
The FeatureStandard's `fingerprint` hashes that intention with the title, the
|
|
46
|
+
project and the priority. It is what tells an **edit** from an **echo** — a
|
|
47
|
+
comment or a state label moves `updated_at` and delivers the issue again, and
|
|
48
|
+
so does a human ticking a box. None of those is a change of mind, so none of
|
|
49
|
+
them changes the fingerprint; a different fingerprint on a Feature in flight
|
|
50
|
+
is recorded as pending and freezes it once the current unit lands
|
|
51
|
+
(PRODUCT.md, "An intention edited while in flight"). Rewording the body is an
|
|
52
|
+
edit. Ticking a box is not.
|
|
53
|
+
|
|
54
|
+
## 4. Ready
|
|
55
|
+
|
|
56
|
+
`signalsReady` is true when the payload carries `readyLabel`, ignoring case, in
|
|
57
|
+
either label spelling GitHub uses (a string, or an object with `name`). A poll
|
|
58
|
+
returns a snapshot with no `labeled` edge, so the label on the snapshot is the
|
|
59
|
+
only signal there is.
|
|
60
|
+
|
|
61
|
+
## 5. Setup
|
|
62
|
+
|
|
63
|
+
`setupManager` is the only part of this package that writes to GitHub before a
|
|
64
|
+
run. It reads the repository once — that single response answers reachable,
|
|
65
|
+
issues enabled, and whether the token may write — then lists the labels the
|
|
66
|
+
configuration names against the labels that exist.
|
|
67
|
+
|
|
68
|
+
Without `apply` it reports a plan and sends no `POST`. With `apply` it creates
|
|
69
|
+
the missing labels and nothing else: an existing label is never renamed,
|
|
70
|
+
recoloured, or deleted, and a repeated name is created once. A second `apply`
|
|
71
|
+
reports everything `satisfied`.
|
|
72
|
+
|
|
73
|
+
A 404 on the repository is reported as "not found, or this token cannot see
|
|
74
|
+
it", because from here the two are the same answer.
|
|
75
|
+
|
|
76
|
+
## 6. Doctor
|
|
77
|
+
|
|
78
|
+
`checkManager` reports the repository, whether the token variable is set
|
|
79
|
+
(`fail` when it is not), whether labels narrow the Source (`warn` when they do
|
|
80
|
+
not — every open issue would be admitted), and reminds which labels the
|
|
81
|
+
repository needs. It never calls the API: the answer must be the same offline.
|
|
82
|
+
|
|
83
|
+
## 6. Init
|
|
84
|
+
|
|
85
|
+
`scaffoldManager` fills `repo`, `tokenEnv`, `labels`, `readyLabel` and
|
|
86
|
+
`defaultProject`. It does not create labels on the repository: that needs the
|
|
87
|
+
network, and the operator still owns the tracker. A `repo` already in
|
|
88
|
+
`managerOptions` is kept.
|
|
89
|
+
|
|
90
|
+
## 7. Probe
|
|
91
|
+
|
|
92
|
+
`probe` recovers the issue from the key (`github:<owner>/<name>#<n>`) and
|
|
93
|
+
`GET`s it. Open is `present`. Closed is `gone` — the same intent as a cancel
|
|
94
|
+
delivery. A 404 or 410 is `gone` too (deleted, or transferred). Anything else
|
|
95
|
+
— a 401, a 403, a 5xx, a body that is not an issue — is `unavailable`. A key
|
|
96
|
+
with no issue number is `unavailable`, not `gone`.
|
|
97
|
+
|
|
98
|
+
`fetchIssue` still treats every failure as `unavailable`: a Fetch that did
|
|
99
|
+
not answer says nothing about the intention being converted. Probe is the
|
|
100
|
+
path that may read a 404 as absence.
|
|
101
|
+
|
|
102
|
+
## 8. Acceptance
|
|
103
|
+
|
|
104
|
+
1. A token in the variable named by `tokenEnv` is enough to build the Port.
|
|
105
|
+
2. No token → a reason naming the variable it looked in.
|
|
106
|
+
3. A missing `repo`, a `repo` that is not `owner/name`, and an unknown option
|
|
107
|
+
key each return a reason.
|
|
108
|
+
4. `checkManager` fails on a missing token and warns when no label is set.
|
|
109
|
+
5. `setup` without `apply` writes nothing; with `apply` it creates only the
|
|
110
|
+
missing labels, and a second `apply` writes nothing at all.
|
|
111
|
+
6. Disabled issues, a read-only token, and an unreachable repository each block
|
|
112
|
+
before any label is created.
|
|
113
|
+
7. `scaffoldManager` keeps a `repo` it was given and names `GITHUB_TOKEN`.
|
|
114
|
+
8. `invalid`, `accepted`, `planned`, `escalated` and `resumed` comments are
|
|
115
|
+
written as string literals, not a field dump. An `invalid` comment is the
|
|
116
|
+
heading and the reason as a paragraph, with no `- reason:` bullet.
|
|
117
|
+
9. `probe` of an open issue is `present`; closed or 404 is `gone`; 403 is
|
|
118
|
+
`unavailable`. A key with no issue number does not call the API.
|
|
119
|
+
10. A template's HTML comments and a task list's boxes leave the intention;
|
|
120
|
+
the bullet and the words stay. The same body with a box ticked normalises
|
|
121
|
+
to the same intention and the same fingerprint. A body that was only
|
|
122
|
+
noise falls back to the title.
|
|
123
|
+
11. `setup` blocks on a `branch` the repository does not have, and names the
|
|
124
|
+
repository's default branch when the config named none.
|
|
125
|
+
|
|
126
|
+
How to build and configure: [README.md](./README.md).
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type GithubClient } from "../../github/client.js";
|
|
2
|
+
import type { Invocation } from "../types.js";
|
|
3
|
+
export type FetchResult =
|
|
4
|
+
/** The issue as the API holds it now: the base the raw intention is layered on. */
|
|
5
|
+
{
|
|
6
|
+
kind: "merged";
|
|
7
|
+
issue: Record<string, unknown>;
|
|
8
|
+
}
|
|
9
|
+
/** Nothing is known about the intention. The same call may answer later. */
|
|
10
|
+
| {
|
|
11
|
+
kind: "unavailable";
|
|
12
|
+
detail: string;
|
|
13
|
+
} | {
|
|
14
|
+
kind: "interrupted";
|
|
15
|
+
};
|
|
16
|
+
export type FetchRequest = {
|
|
17
|
+
invocation: Invocation;
|
|
18
|
+
github: GithubClient;
|
|
19
|
+
number: number;
|
|
20
|
+
deadlineMs: number;
|
|
21
|
+
now: () => number;
|
|
22
|
+
shouldInterrupt: () => boolean;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Re-read the issue. Every failure is `unavailable`, never `invalid`: a Fetch
|
|
26
|
+
* that did not answer says nothing about the intention.
|
|
27
|
+
*/
|
|
28
|
+
export declare function fetchIssue(request: FetchRequest): Promise<FetchResult>;
|
|
29
|
+
//# sourceMappingURL=fetch-issue.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch-issue.d.ts","sourceRoot":"","sources":["../../../src/adapter/github/fetch-issue.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,YAAY,EAAe,MAAM,wBAAwB,CAAC;AACpF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C,MAAM,MAAM,WAAW;AACrB,mFAAmF;AACjF;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE;AACpD,4EAA4E;GAC1E;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACvC;IAAE,IAAI,EAAE,aAAa,CAAA;CAAE,CAAC;AAE5B,MAAM,MAAM,YAAY,GAAG;IACzB,UAAU,EAAE,UAAU,CAAC;IACvB,MAAM,EAAE,YAAY,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,MAAM,CAAC;IAClB,eAAe,EAAE,MAAM,OAAO,CAAC;CAChC,CAAC;AAEF;;;GAGG;AACH,wBAAsB,UAAU,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CA4B5E"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { callSignal, readFailure } from "../../github/client.js";
|
|
2
|
+
/**
|
|
3
|
+
* Re-read the issue. Every failure is `unavailable`, never `invalid`: a Fetch
|
|
4
|
+
* that did not answer says nothing about the intention.
|
|
5
|
+
*/
|
|
6
|
+
export async function fetchIssue(request) {
|
|
7
|
+
const call = callSignal(request.deadlineMs, request.now, request.shouldInterrupt);
|
|
8
|
+
try {
|
|
9
|
+
const response = await request.github.request("GET /repos/{owner}/{repo}/issues/{issue_number}", {
|
|
10
|
+
owner: request.invocation.repo.owner,
|
|
11
|
+
repo: request.invocation.repo.name,
|
|
12
|
+
issue_number: request.number,
|
|
13
|
+
request: { signal: call.signal },
|
|
14
|
+
});
|
|
15
|
+
const issue = response.data;
|
|
16
|
+
if (issue === null || typeof issue !== "object" || Array.isArray(issue)) {
|
|
17
|
+
return {
|
|
18
|
+
kind: "unavailable",
|
|
19
|
+
detail: "The Fetch answered with something other than an issue.",
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
return { kind: "merged", issue: issue };
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
if (request.shouldInterrupt()) {
|
|
26
|
+
return { kind: "interrupted" };
|
|
27
|
+
}
|
|
28
|
+
return { kind: "unavailable", detail: readFailure(error).detail };
|
|
29
|
+
}
|
|
30
|
+
finally {
|
|
31
|
+
call.release();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=fetch-issue.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch-issue.js","sourceRoot":"","sources":["../../../src/adapter/github/fetch-issue.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAqB,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAmBpF;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,OAAqB;IACpD,MAAM,IAAI,GAAG,UAAU,CAAC,OAAO,CAAC,UAAU,EAAE,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,eAAe,CAAC,CAAC;IAClF,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,MAAM,CAAC,OAAO,CAC3C,iDAAiD,EACjD;YACE,KAAK,EAAE,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC,KAAK;YACpC,IAAI,EAAE,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI;YAClC,YAAY,EAAE,OAAO,CAAC,MAAM;YAC5B,OAAO,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE;SACjC,CACF,CAAC;QACF,MAAM,KAAK,GAAG,QAAQ,CAAC,IAAe,CAAC;QACvC,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YACxE,OAAO;gBACL,IAAI,EAAE,aAAa;gBACnB,MAAM,EAAE,wDAAwD;aACjE,CAAC;QACJ,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAgC,EAAE,CAAC;IACrE,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,OAAO,CAAC,eAAe,EAAE,EAAE,CAAC;YAC9B,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,CAAC;QACjC,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,EAAE,WAAW,CAAC,KAAK,CAAC,CAAC,MAAM,EAAE,CAAC;IACpE,CAAC;YAAS,CAAC;QACT,IAAI,CAAC,OAAO,EAAE,CAAC;IACjB,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading an issue body. Pure: no clock, no request, no file.
|
|
3
|
+
*
|
|
4
|
+
* A body is free text a human wrote in a web form. Two shapes are recognised —
|
|
5
|
+
* a task list, and an acceptance section — because those are the two shapes
|
|
6
|
+
* GitHub itself renders as a checklist. Everything else stays intention text.
|
|
7
|
+
*/
|
|
8
|
+
export type BodyParts = {
|
|
9
|
+
/** The free text left once the criteria were taken out; absent when empty. */
|
|
10
|
+
intention?: string;
|
|
11
|
+
criteria: string[];
|
|
12
|
+
};
|
|
13
|
+
/** Comments an issue template left behind are not something a human wrote. */
|
|
14
|
+
export declare function stripComments(body: string): string;
|
|
15
|
+
/**
|
|
16
|
+
* Split a body into the intention and its acceptance criteria.
|
|
17
|
+
*
|
|
18
|
+
* Task-list items win wherever they are: `- [ ] …` is the checklist GitHub
|
|
19
|
+
* renders, and a human who wrote one meant it. Without any, a section titled
|
|
20
|
+
* "acceptance criteria" (or "definition of done") is read instead, and the whole
|
|
21
|
+
* section leaves the intention.
|
|
22
|
+
*/
|
|
23
|
+
export declare function readBody(body: string): BodyParts;
|
|
24
|
+
//# sourceMappingURL=body.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"body.d.ts","sourceRoot":"","sources":["../../../src/adapter/normalize/body.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AASH,MAAM,MAAM,SAAS,GAAG;IACtB,8EAA8E;IAC9E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB,CAAC;AAEF,8EAA8E;AAC9E,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAElD;AAcD;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAkChD"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading an issue body. Pure: no clock, no request, no file.
|
|
3
|
+
*
|
|
4
|
+
* A body is free text a human wrote in a web form. Two shapes are recognised —
|
|
5
|
+
* a task list, and an acceptance section — because those are the two shapes
|
|
6
|
+
* GitHub itself renders as a checklist. Everything else stays intention text.
|
|
7
|
+
*/
|
|
8
|
+
const TASK_ITEM = /^\s*[-*+]\s+\[[ xX]\]\s+(.*\S)\s*$/;
|
|
9
|
+
const HEADING = /^\s{0,3}#{1,6}\s+(.*\S)\s*$/;
|
|
10
|
+
const BULLET = /^\s*(?:[-*+]|\d+[.)])\s+(.*\S)\s*$/;
|
|
11
|
+
/** Headings whose section is a list of criteria rather than intention text. */
|
|
12
|
+
const CRITERIA_HEADINGS = new Set(["acceptance criteria", "definition of done"]);
|
|
13
|
+
/** Comments an issue template left behind are not something a human wrote. */
|
|
14
|
+
export function stripComments(body) {
|
|
15
|
+
return body.replace(/<!--[\s\S]*?-->/g, "");
|
|
16
|
+
}
|
|
17
|
+
function headingTitle(line) {
|
|
18
|
+
const matched = HEADING.exec(line);
|
|
19
|
+
return matched?.[1] === undefined ? null : normalizeTitle(matched[1]);
|
|
20
|
+
}
|
|
21
|
+
function normalizeTitle(title) {
|
|
22
|
+
return title
|
|
23
|
+
.toLowerCase()
|
|
24
|
+
.replace(/[^a-z0-9]+/g, " ")
|
|
25
|
+
.trim();
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Split a body into the intention and its acceptance criteria.
|
|
29
|
+
*
|
|
30
|
+
* Task-list items win wherever they are: `- [ ] …` is the checklist GitHub
|
|
31
|
+
* renders, and a human who wrote one meant it. Without any, a section titled
|
|
32
|
+
* "acceptance criteria" (or "definition of done") is read instead, and the whole
|
|
33
|
+
* section leaves the intention.
|
|
34
|
+
*/
|
|
35
|
+
export function readBody(body) {
|
|
36
|
+
const lines = stripComments(body).replace(/\r\n?/g, "\n").split("\n");
|
|
37
|
+
const criteria = [];
|
|
38
|
+
const kept = [];
|
|
39
|
+
for (const line of lines) {
|
|
40
|
+
const task = TASK_ITEM.exec(line);
|
|
41
|
+
if (task?.[1] !== undefined) {
|
|
42
|
+
criteria.push(task[1].trim());
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
kept.push(line);
|
|
46
|
+
}
|
|
47
|
+
if (criteria.length > 0) {
|
|
48
|
+
return parts(dropEmptiedCriteriaHeadings(kept), criteria);
|
|
49
|
+
}
|
|
50
|
+
const start = lines.findIndex((line) => {
|
|
51
|
+
const title = headingTitle(line);
|
|
52
|
+
return title !== null && CRITERIA_HEADINGS.has(title);
|
|
53
|
+
});
|
|
54
|
+
if (start === -1) {
|
|
55
|
+
return parts(lines, []);
|
|
56
|
+
}
|
|
57
|
+
let end = start + 1;
|
|
58
|
+
while (end < lines.length && headingTitle(lines[end] ?? "") === null) {
|
|
59
|
+
const bullet = BULLET.exec(lines[end] ?? "");
|
|
60
|
+
if (bullet?.[1] !== undefined) {
|
|
61
|
+
criteria.push(bullet[1].trim());
|
|
62
|
+
}
|
|
63
|
+
end += 1;
|
|
64
|
+
}
|
|
65
|
+
return parts([...lines.slice(0, start), ...lines.slice(end)], criteria);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Drop a criteria heading whose section the task list just emptied.
|
|
69
|
+
*
|
|
70
|
+
* `## Acceptance criteria` followed by `- [ ] …` items loses its items to the
|
|
71
|
+
* task list above, and a bare heading with nothing under it would otherwise
|
|
72
|
+
* travel on as intention text — into the Plan, and into the Builder's prompt.
|
|
73
|
+
* A heading that still has prose under it stays: that prose is intention.
|
|
74
|
+
*/
|
|
75
|
+
function dropEmptiedCriteriaHeadings(lines) {
|
|
76
|
+
const out = [];
|
|
77
|
+
for (let i = 0; i < lines.length; i++) {
|
|
78
|
+
const title = headingTitle(lines[i] ?? "");
|
|
79
|
+
if (title === null || !CRITERIA_HEADINGS.has(title)) {
|
|
80
|
+
out.push(lines[i] ?? "");
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
let end = i + 1;
|
|
84
|
+
while (end < lines.length && headingTitle(lines[end] ?? "") === null) {
|
|
85
|
+
end += 1;
|
|
86
|
+
}
|
|
87
|
+
if (lines.slice(i + 1, end).some((line) => line.trim().length > 0)) {
|
|
88
|
+
out.push(...lines.slice(i, end));
|
|
89
|
+
}
|
|
90
|
+
i = end - 1;
|
|
91
|
+
}
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
function parts(kept, criteria) {
|
|
95
|
+
const intention = kept
|
|
96
|
+
.join("\n")
|
|
97
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
98
|
+
.trim();
|
|
99
|
+
return {
|
|
100
|
+
...(intention.length > 0 ? { intention } : {}),
|
|
101
|
+
criteria: criteria.filter((entry) => entry.length > 0),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=body.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"body.js","sourceRoot":"","sources":["../../../src/adapter/normalize/body.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** JSON with object keys sorted, so two equal documents hash the same. */
|
|
2
|
+
export declare function canonicalJson(value: unknown): string;
|
|
3
|
+
/**
|
|
4
|
+
* Answers "did this intention change?". Covers every field of the
|
|
5
|
+
* FeatureStandard except the two that are not the intention itself.
|
|
6
|
+
*/
|
|
7
|
+
export declare function fingerprintOf(document: Record<string, unknown>): string;
|
|
8
|
+
//# sourceMappingURL=fingerprint.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fingerprint.d.ts","sourceRoot":"","sources":["../../../src/adapter/normalize/fingerprint.ts"],"names":[],"mappings":"AAEA,0EAA0E;AAC1E,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAYpD;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAKvE"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
/** JSON with object keys sorted, so two equal documents hash the same. */
|
|
3
|
+
export function canonicalJson(value) {
|
|
4
|
+
if (value === null || typeof value !== "object") {
|
|
5
|
+
return JSON.stringify(value) ?? "null";
|
|
6
|
+
}
|
|
7
|
+
if (Array.isArray(value)) {
|
|
8
|
+
return `[${value.map(canonicalJson).join(",")}]`;
|
|
9
|
+
}
|
|
10
|
+
const entries = Object.entries(value)
|
|
11
|
+
.filter(([, entryValue]) => entryValue !== undefined)
|
|
12
|
+
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
|
|
13
|
+
.map(([key, entryValue]) => `${JSON.stringify(key)}:${canonicalJson(entryValue)}`);
|
|
14
|
+
return `{${entries.join(",")}}`;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Answers "did this intention change?". Covers every field of the
|
|
18
|
+
* FeatureStandard except the two that are not the intention itself.
|
|
19
|
+
*/
|
|
20
|
+
export function fingerprintOf(document) {
|
|
21
|
+
const subject = { ...document };
|
|
22
|
+
delete subject.fingerprint;
|
|
23
|
+
delete subject.normalized_at;
|
|
24
|
+
return `sha256:${createHash("sha256").update(canonicalJson(subject)).digest("hex")}`;
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=fingerprint.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fingerprint.js","sourceRoot":"","sources":["../../../src/adapter/normalize/fingerprint.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC,0EAA0E;AAC1E,MAAM,UAAU,aAAa,CAAC,KAAc;IAC1C,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAChD,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,MAAM,CAAC;IACzC,CAAC;IACD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,IAAI,KAAK,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;IACnD,CAAC;IACD,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,KAAgC,CAAC;SAC7D,MAAM,CAAC,CAAC,CAAC,EAAE,UAAU,CAAC,EAAE,EAAE,CAAC,UAAU,KAAK,SAAS,CAAC;SACpD,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;SACrE,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,UAAU,CAAC,EAAE,EAAE,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,aAAa,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC;IACrF,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;AAClC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,QAAiC;IAC7D,MAAM,OAAO,GAA4B,EAAE,GAAG,QAAQ,EAAE,CAAC;IACzD,OAAO,OAAO,CAAC,WAAW,CAAC;IAC3B,OAAO,OAAO,CAAC,aAAa,CAAC;IAC7B,OAAO,UAAU,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;AACvF,CAAC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Intent, Invalid } from "../types.js";
|
|
2
|
+
/** What a repository can say about an issue, in this Block's five words. */
|
|
3
|
+
export type Kind = "create" | "update" | "ready" | "cancel" | "delete";
|
|
4
|
+
export type IntentInput = {
|
|
5
|
+
action: string | undefined;
|
|
6
|
+
state: string | undefined;
|
|
7
|
+
labels: string[];
|
|
8
|
+
readyLabel: string;
|
|
9
|
+
};
|
|
10
|
+
export type IntentResult = {
|
|
11
|
+
ok: true;
|
|
12
|
+
intent: Intent;
|
|
13
|
+
} | {
|
|
14
|
+
ok: false;
|
|
15
|
+
invalid: Invalid;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Collapse what happened into one of three intents.
|
|
19
|
+
*
|
|
20
|
+
* With an action, the repository said what changed. Without one — a snapshot a
|
|
21
|
+
* subscription listed — only the state is known: open means the intention
|
|
22
|
+
* itself, closed means drop it. `ready` needs the edge, not the state: it is the
|
|
23
|
+
* moment the label arrived, and a snapshot cannot tell that from a label that
|
|
24
|
+
* has been there for a week.
|
|
25
|
+
*/
|
|
26
|
+
export declare function readIntent(input: IntentInput): IntentResult;
|
|
27
|
+
//# sourceMappingURL=intent.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"intent.d.ts","sourceRoot":"","sources":["../../../src/adapter/normalize/intent.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAGnD,4EAA4E;AAC5E,MAAM,MAAM,IAAI,GAAG,QAAQ,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,GAAG,QAAQ,CAAC;AA+BvE,MAAM,MAAM,WAAW,GAAG;IACxB,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC;AAE1F;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,WAAW,GAAG,YAAY,CAmB3D"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { hasLabel } from "./labels.js";
|
|
2
|
+
const ACTION_TO_KIND = {
|
|
3
|
+
opened: "create",
|
|
4
|
+
reopened: "create",
|
|
5
|
+
edited: "update",
|
|
6
|
+
labeled: "update",
|
|
7
|
+
unlabeled: "update",
|
|
8
|
+
assigned: "update",
|
|
9
|
+
unassigned: "update",
|
|
10
|
+
milestoned: "update",
|
|
11
|
+
demilestoned: "update",
|
|
12
|
+
pinned: "update",
|
|
13
|
+
unpinned: "update",
|
|
14
|
+
locked: "update",
|
|
15
|
+
unlocked: "update",
|
|
16
|
+
typed: "update",
|
|
17
|
+
untyped: "update",
|
|
18
|
+
closed: "cancel",
|
|
19
|
+
deleted: "delete",
|
|
20
|
+
transferred: "delete",
|
|
21
|
+
};
|
|
22
|
+
const KIND_TO_INTENT = {
|
|
23
|
+
create: "upsert",
|
|
24
|
+
update: "upsert",
|
|
25
|
+
ready: "ready",
|
|
26
|
+
cancel: "cancel",
|
|
27
|
+
delete: "cancel",
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Collapse what happened into one of three intents.
|
|
31
|
+
*
|
|
32
|
+
* With an action, the repository said what changed. Without one — a snapshot a
|
|
33
|
+
* subscription listed — only the state is known: open means the intention
|
|
34
|
+
* itself, closed means drop it. `ready` needs the edge, not the state: it is the
|
|
35
|
+
* moment the label arrived, and a snapshot cannot tell that from a label that
|
|
36
|
+
* has been there for a week.
|
|
37
|
+
*/
|
|
38
|
+
export function readIntent(input) {
|
|
39
|
+
if (input.action === undefined) {
|
|
40
|
+
return { ok: true, intent: input.state === "closed" ? "cancel" : "upsert" };
|
|
41
|
+
}
|
|
42
|
+
const kind = ACTION_TO_KIND[input.action];
|
|
43
|
+
if (kind === undefined) {
|
|
44
|
+
return {
|
|
45
|
+
ok: false,
|
|
46
|
+
invalid: {
|
|
47
|
+
code: "unknown-action",
|
|
48
|
+
reason: `Action "${input.action}" is not one this Block reads.`,
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
if (input.action === "labeled" && hasLabel(input.labels, input.readyLabel)) {
|
|
53
|
+
return { ok: true, intent: KIND_TO_INTENT.ready };
|
|
54
|
+
}
|
|
55
|
+
return { ok: true, intent: KIND_TO_INTENT[kind] };
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=intent.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"intent.js","sourceRoot":"","sources":["../../../src/adapter/normalize/intent.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAKvC,MAAM,cAAc,GAAyB;IAC3C,MAAM,EAAE,QAAQ;IAChB,QAAQ,EAAE,QAAQ;IAClB,MAAM,EAAE,QAAQ;IAChB,OAAO,EAAE,QAAQ;IACjB,SAAS,EAAE,QAAQ;IACnB,QAAQ,EAAE,QAAQ;IAClB,UAAU,EAAE,QAAQ;IACpB,UAAU,EAAE,QAAQ;IACpB,YAAY,EAAE,QAAQ;IACtB,MAAM,EAAE,QAAQ;IAChB,QAAQ,EAAE,QAAQ;IAClB,MAAM,EAAE,QAAQ;IAChB,QAAQ,EAAE,QAAQ;IAClB,KAAK,EAAE,QAAQ;IACf,OAAO,EAAE,QAAQ;IACjB,MAAM,EAAE,QAAQ;IAChB,OAAO,EAAE,QAAQ;IACjB,WAAW,EAAE,QAAQ;CACtB,CAAC;AAEF,MAAM,cAAc,GAAyB;IAC3C,MAAM,EAAE,QAAQ;IAChB,MAAM,EAAE,QAAQ;IAChB,KAAK,EAAE,OAAO;IACd,MAAM,EAAE,QAAQ;IAChB,MAAM,EAAE,QAAQ;CACjB,CAAC;AAWF;;;;;;;;GAQG;AACH,MAAM,UAAU,UAAU,CAAC,KAAkB;IAC3C,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QAC/B,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;IAC9E,CAAC;IAED,MAAM,IAAI,GAAG,cAAc,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IAC1C,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,OAAO;YACL,EAAE,EAAE,KAAK;YACT,OAAO,EAAE;gBACP,IAAI,EAAE,gBAAgB;gBACtB,MAAM,EAAE,WAAW,KAAK,CAAC,MAAM,gCAAgC;aAChE;SACF,CAAC;IACJ,CAAC;IACD,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS,IAAI,QAAQ,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,UAAU,CAAC,EAAE,CAAC;QAC3E,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,cAAc,CAAC,KAAK,EAAE,CAAC;IACpD,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,cAAc,CAAC,IAAI,CAAC,EAAE,CAAC;AACpD,CAAC"}
|