@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.
Files changed (155) hide show
  1. package/README.md +162 -0
  2. package/SPECS.md +126 -0
  3. package/dist/adapter/github/fetch-issue.d.ts +29 -0
  4. package/dist/adapter/github/fetch-issue.d.ts.map +1 -0
  5. package/dist/adapter/github/fetch-issue.js +34 -0
  6. package/dist/adapter/github/fetch-issue.js.map +1 -0
  7. package/dist/adapter/normalize/body.d.ts +24 -0
  8. package/dist/adapter/normalize/body.d.ts.map +1 -0
  9. package/dist/adapter/normalize/body.js +104 -0
  10. package/dist/adapter/normalize/body.js.map +1 -0
  11. package/dist/adapter/normalize/fingerprint.d.ts +8 -0
  12. package/dist/adapter/normalize/fingerprint.d.ts.map +1 -0
  13. package/dist/adapter/normalize/fingerprint.js +26 -0
  14. package/dist/adapter/normalize/fingerprint.js.map +1 -0
  15. package/dist/adapter/normalize/intent.d.ts +27 -0
  16. package/dist/adapter/normalize/intent.d.ts.map +1 -0
  17. package/dist/adapter/normalize/intent.js +57 -0
  18. package/dist/adapter/normalize/intent.js.map +1 -0
  19. package/dist/adapter/normalize/labels.d.ts +17 -0
  20. package/dist/adapter/normalize/labels.d.ts.map +1 -0
  21. package/dist/adapter/normalize/labels.js +43 -0
  22. package/dist/adapter/normalize/labels.js.map +1 -0
  23. package/dist/adapter/normalize/noise.d.ts +13 -0
  24. package/dist/adapter/normalize/noise.d.ts.map +1 -0
  25. package/dist/adapter/normalize/noise.js +28 -0
  26. package/dist/adapter/normalize/noise.js.map +1 -0
  27. package/dist/adapter/normalize/normalize.d.ts +22 -0
  28. package/dist/adapter/normalize/normalize.d.ts.map +1 -0
  29. package/dist/adapter/normalize/normalize.js +216 -0
  30. package/dist/adapter/normalize/normalize.js.map +1 -0
  31. package/dist/adapter/progress/emit.d.ts +3 -0
  32. package/dist/adapter/progress/emit.d.ts.map +1 -0
  33. package/dist/adapter/progress/emit.js +6 -0
  34. package/dist/adapter/progress/emit.js.map +1 -0
  35. package/dist/adapter/raw/parse-raw.d.ts +14 -0
  36. package/dist/adapter/raw/parse-raw.d.ts.map +1 -0
  37. package/dist/adapter/raw/parse-raw.js +47 -0
  38. package/dist/adapter/raw/parse-raw.js.map +1 -0
  39. package/dist/adapter/raw/unwrap.d.ts +12 -0
  40. package/dist/adapter/raw/unwrap.d.ts.map +1 -0
  41. package/dist/adapter/raw/unwrap.js +19 -0
  42. package/dist/adapter/raw/unwrap.js.map +1 -0
  43. package/dist/adapter/run/run-adapter.d.ts +28 -0
  44. package/dist/adapter/run/run-adapter.d.ts.map +1 -0
  45. package/dist/adapter/run/run-adapter.js +131 -0
  46. package/dist/adapter/run/run-adapter.js.map +1 -0
  47. package/dist/adapter/types.d.ts +69 -0
  48. package/dist/adapter/types.d.ts.map +1 -0
  49. package/dist/adapter/types.js +3 -0
  50. package/dist/adapter/types.js.map +1 -0
  51. package/dist/emitter/event/field-matrix.d.ts +29 -0
  52. package/dist/emitter/event/field-matrix.d.ts.map +1 -0
  53. package/dist/emitter/event/field-matrix.js +94 -0
  54. package/dist/emitter/event/field-matrix.js.map +1 -0
  55. package/dist/emitter/github/comments.d.ts +14 -0
  56. package/dist/emitter/github/comments.d.ts.map +1 -0
  57. package/dist/emitter/github/comments.js +69 -0
  58. package/dist/emitter/github/comments.js.map +1 -0
  59. package/dist/emitter/github/labels.d.ts +18 -0
  60. package/dist/emitter/github/labels.d.ts.map +1 -0
  61. package/dist/emitter/github/labels.js +81 -0
  62. package/dist/emitter/github/labels.js.map +1 -0
  63. package/dist/emitter/github/result.d.ts +19 -0
  64. package/dist/emitter/github/result.d.ts.map +1 -0
  65. package/dist/emitter/github/result.js +2 -0
  66. package/dist/emitter/github/result.js.map +1 -0
  67. package/dist/emitter/progress/emit.d.ts +3 -0
  68. package/dist/emitter/progress/emit.d.ts.map +1 -0
  69. package/dist/emitter/progress/emit.js +6 -0
  70. package/dist/emitter/progress/emit.js.map +1 -0
  71. package/dist/emitter/render/render-event.d.ts +20 -0
  72. package/dist/emitter/render/render-event.d.ts.map +1 -0
  73. package/dist/emitter/render/render-event.js +293 -0
  74. package/dist/emitter/render/render-event.js.map +1 -0
  75. package/dist/emitter/render/truncate.d.ts +9 -0
  76. package/dist/emitter/render/truncate.d.ts.map +1 -0
  77. package/dist/emitter/render/truncate.js +10 -0
  78. package/dist/emitter/render/truncate.js.map +1 -0
  79. package/dist/emitter/run/run-emitter.d.ts +27 -0
  80. package/dist/emitter/run/run-emitter.d.ts.map +1 -0
  81. package/dist/emitter/run/run-emitter.js +121 -0
  82. package/dist/emitter/run/run-emitter.js.map +1 -0
  83. package/dist/emitter/thread/marker.d.ts +15 -0
  84. package/dist/emitter/thread/marker.d.ts.map +1 -0
  85. package/dist/emitter/thread/marker.js +37 -0
  86. package/dist/emitter/thread/marker.js.map +1 -0
  87. package/dist/emitter/types.d.ts +64 -0
  88. package/dist/emitter/types.d.ts.map +1 -0
  89. package/dist/emitter/types.js +3 -0
  90. package/dist/emitter/types.js.map +1 -0
  91. package/dist/github/client.d.ts +63 -0
  92. package/dist/github/client.d.ts.map +1 -0
  93. package/dist/github/client.js +96 -0
  94. package/dist/github/client.js.map +1 -0
  95. package/dist/index.d.ts +55 -0
  96. package/dist/index.d.ts.map +1 -0
  97. package/dist/index.js +357 -0
  98. package/dist/index.js.map +1 -0
  99. package/dist/issue-number.d.ts +5 -0
  100. package/dist/issue-number.d.ts.map +1 -0
  101. package/dist/issue-number.js +32 -0
  102. package/dist/issue-number.js.map +1 -0
  103. package/dist/labels.d.ts +3 -0
  104. package/dist/labels.d.ts.map +1 -0
  105. package/dist/labels.js +24 -0
  106. package/dist/labels.js.map +1 -0
  107. package/dist/listener/delivery/run-hook.d.ts +8 -0
  108. package/dist/listener/delivery/run-hook.d.ts.map +1 -0
  109. package/dist/listener/delivery/run-hook.js +50 -0
  110. package/dist/listener/delivery/run-hook.js.map +1 -0
  111. package/dist/listener/github/list-issues.d.ts +30 -0
  112. package/dist/listener/github/list-issues.d.ts.map +1 -0
  113. package/dist/listener/github/list-issues.js +36 -0
  114. package/dist/listener/github/list-issues.js.map +1 -0
  115. package/dist/listener/progress/emit.d.ts +3 -0
  116. package/dist/listener/progress/emit.d.ts.map +1 -0
  117. package/dist/listener/progress/emit.js +6 -0
  118. package/dist/listener/progress/emit.js.map +1 -0
  119. package/dist/listener/run/run-listener.d.ts +35 -0
  120. package/dist/listener/run/run-listener.d.ts.map +1 -0
  121. package/dist/listener/run/run-listener.js +217 -0
  122. package/dist/listener/run/run-listener.js.map +1 -0
  123. package/dist/listener/source/select-events.d.ts +46 -0
  124. package/dist/listener/source/select-events.d.ts.map +1 -0
  125. package/dist/listener/source/select-events.js +102 -0
  126. package/dist/listener/source/select-events.js.map +1 -0
  127. package/dist/listener/types.d.ts +56 -0
  128. package/dist/listener/types.d.ts.map +1 -0
  129. package/dist/listener/types.js +3 -0
  130. package/dist/listener/types.js.map +1 -0
  131. package/dist/manager.d.ts +29 -0
  132. package/dist/manager.d.ts.map +1 -0
  133. package/dist/manager.js +256 -0
  134. package/dist/manager.js.map +1 -0
  135. package/dist/probe/probe-issue.d.ts +20 -0
  136. package/dist/probe/probe-issue.d.ts.map +1 -0
  137. package/dist/probe/probe-issue.js +45 -0
  138. package/dist/probe/probe-issue.js.map +1 -0
  139. package/dist/repo.d.ts +6 -0
  140. package/dist/repo.d.ts.map +1 -0
  141. package/dist/repo.js +14 -0
  142. package/dist/repo.js.map +1 -0
  143. package/dist/setup.d.ts +43 -0
  144. package/dist/setup.d.ts.map +1 -0
  145. package/dist/setup.js +281 -0
  146. package/dist/setup.js.map +1 -0
  147. package/dist/submission/submission.d.ts +44 -0
  148. package/dist/submission/submission.d.ts.map +1 -0
  149. package/dist/submission/submission.js +189 -0
  150. package/dist/submission/submission.js.map +1 -0
  151. package/dist/written-fingerprint.d.ts +7 -0
  152. package/dist/written-fingerprint.d.ts.map +1 -0
  153. package/dist/written-fingerprint.js +38 -0
  154. package/dist/written-fingerprint.js.map +1 -0
  155. 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"}