@volter/twin-github 0.1.2 → 2.0.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.
Files changed (215) hide show
  1. package/README.md +88 -36
  2. package/client/github-mirror.css +277 -319
  3. package/client/github-mirror.d.ts +418 -0
  4. package/client/github-mirror.js +485 -0
  5. package/client/github-mirror.tsx +153 -357
  6. package/client/pulls-rest.ts +159 -0
  7. package/client/pulls-workspace.tsx +841 -0
  8. package/dist/client/github-mirror.bundle.js +239 -0
  9. package/dist/client/github-mirror.css +916 -0
  10. package/dist/client/github-mirror.d.ts +418 -0
  11. package/dist/client/github-mirror.js +485 -0
  12. package/dist/client/github-mirror.tsx +1315 -0
  13. package/dist/client/pulls-rest.d.ts +42 -0
  14. package/dist/client/pulls-rest.js +140 -0
  15. package/dist/client/pulls-rest.ts +159 -0
  16. package/dist/client/pulls-workspace.bundle.js +22 -0
  17. package/dist/client/pulls-workspace.d.ts +114 -0
  18. package/dist/client/pulls-workspace.js +418 -0
  19. package/dist/client/pulls-workspace.tsx +841 -0
  20. package/dist/src/cli.d.ts +2 -0
  21. package/dist/src/cli.js +40 -0
  22. package/dist/src/generated/graphql-sdl.gen.json +1 -0
  23. package/dist/src/generated/graphql.gen.json +1 -0
  24. package/dist/src/generated/surface.gen.json +1 -0
  25. package/dist/src/generated/ui.gen.json +1 -0
  26. package/dist/src/github-budget.d.ts +69 -0
  27. package/dist/src/github-budget.js +172 -0
  28. package/dist/src/github-capabilities.d.ts +5 -0
  29. package/dist/src/github-capabilities.js +4468 -0
  30. package/dist/src/github-conformance.d.ts +43 -0
  31. package/dist/src/github-conformance.js +76 -0
  32. package/dist/src/github-connector.d.ts +307 -0
  33. package/dist/src/github-connector.js +1398 -0
  34. package/dist/src/github-events.d.ts +41 -0
  35. package/dist/src/github-events.js +232 -0
  36. package/dist/src/github-git-http.d.ts +49 -0
  37. package/dist/src/github-git-http.js +185 -0
  38. package/dist/src/github-git-plane.d.ts +114 -0
  39. package/dist/src/github-git-plane.js +407 -0
  40. package/dist/src/github-mirror-state.d.ts +2 -0
  41. package/dist/src/github-mirror-state.js +335 -0
  42. package/dist/src/github-mirror-ui.d.ts +20 -0
  43. package/dist/src/github-mirror-ui.js +101 -0
  44. package/dist/src/github-server.d.ts +14 -0
  45. package/dist/src/github-server.js +240 -0
  46. package/dist/src/github-shared.d.ts +12 -0
  47. package/dist/src/github-shared.js +21 -0
  48. package/dist/src/github-twin.d.ts +1411 -0
  49. package/dist/src/github-twin.js +4084 -0
  50. package/dist/src/github-ui-conformance.d.ts +4 -0
  51. package/dist/src/github-ui-conformance.js +105 -0
  52. package/dist/src/github-ui-structure.d.ts +18 -0
  53. package/dist/src/github-ui-structure.js +251 -0
  54. package/dist/src/graphql-wire.d.ts +22 -0
  55. package/dist/src/graphql-wire.js +89 -0
  56. package/dist/src/index.d.ts +17 -0
  57. package/dist/src/index.js +104 -0
  58. package/dist/src/manifest.d.ts +8 -0
  59. package/dist/src/manifest.js +597 -0
  60. package/dist/src/npm-registry.d.ts +7 -0
  61. package/dist/src/npm-registry.js +47 -0
  62. package/dist/src/screens/app-installation.d.ts +3 -0
  63. package/dist/src/screens/app-installation.js +166 -0
  64. package/dist/src/screens/app-manifest.d.ts +3 -0
  65. package/dist/src/screens/app-manifest.js +81 -0
  66. package/dist/src/screens/oauth.d.ts +15 -0
  67. package/dist/src/screens/oauth.js +257 -0
  68. package/dist/src/screens/session.d.ts +8 -0
  69. package/dist/src/screens/session.js +170 -0
  70. package/dist/src/semantics/actions.d.ts +2 -0
  71. package/dist/src/semantics/actions.js +413 -0
  72. package/dist/src/semantics/activity.d.ts +4 -0
  73. package/dist/src/semantics/activity.js +161 -0
  74. package/dist/src/semantics/apps.d.ts +2 -0
  75. package/dist/src/semantics/apps.js +144 -0
  76. package/dist/src/semantics/branches.d.ts +2 -0
  77. package/dist/src/semantics/branches.js +136 -0
  78. package/dist/src/semantics/checks.d.ts +2 -0
  79. package/dist/src/semantics/checks.js +176 -0
  80. package/dist/src/semantics/code-scanning-upload.d.ts +2 -0
  81. package/dist/src/semantics/code-scanning-upload.js +97 -0
  82. package/dist/src/semantics/codespaces.d.ts +2 -0
  83. package/dist/src/semantics/codespaces.js +58 -0
  84. package/dist/src/semantics/commits.d.ts +2 -0
  85. package/dist/src/semantics/commits.js +109 -0
  86. package/dist/src/semantics/contents.d.ts +2 -0
  87. package/dist/src/semantics/contents.js +131 -0
  88. package/dist/src/semantics/deployments.d.ts +2 -0
  89. package/dist/src/semantics/deployments.js +127 -0
  90. package/dist/src/semantics/gists.d.ts +2 -0
  91. package/dist/src/semantics/gists.js +86 -0
  92. package/dist/src/semantics/git.d.ts +2 -0
  93. package/dist/src/semantics/git.js +235 -0
  94. package/dist/src/semantics/graphql.d.ts +7 -0
  95. package/dist/src/semantics/graphql.js +512 -0
  96. package/dist/src/semantics/index.d.ts +5 -0
  97. package/dist/src/semantics/index.js +62 -0
  98. package/dist/src/semantics/issues.d.ts +2 -0
  99. package/dist/src/semantics/issues.js +456 -0
  100. package/dist/src/semantics/keys.d.ts +2 -0
  101. package/dist/src/semantics/keys.js +66 -0
  102. package/dist/src/semantics/labels.d.ts +2 -0
  103. package/dist/src/semantics/labels.js +100 -0
  104. package/dist/src/semantics/meta.d.ts +12 -0
  105. package/dist/src/semantics/meta.js +144 -0
  106. package/dist/src/semantics/notifications.d.ts +4 -0
  107. package/dist/src/semantics/notifications.js +62 -0
  108. package/dist/src/semantics/orgs.d.ts +2 -0
  109. package/dist/src/semantics/orgs.js +420 -0
  110. package/dist/src/semantics/packages.d.ts +2 -0
  111. package/dist/src/semantics/packages.js +55 -0
  112. package/dist/src/semantics/pages.d.ts +2 -0
  113. package/dist/src/semantics/pages.js +176 -0
  114. package/dist/src/semantics/projects.d.ts +2 -0
  115. package/dist/src/semantics/projects.js +239 -0
  116. package/dist/src/semantics/pulls.d.ts +2 -0
  117. package/dist/src/semantics/pulls.js +462 -0
  118. package/dist/src/semantics/push-reactions.d.ts +101 -0
  119. package/dist/src/semantics/push-reactions.js +513 -0
  120. package/dist/src/semantics/releases.d.ts +19 -0
  121. package/dist/src/semantics/releases.js +231 -0
  122. package/dist/src/semantics/repo-invitations.d.ts +2 -0
  123. package/dist/src/semantics/repo-invitations.js +98 -0
  124. package/dist/src/semantics/repos.d.ts +17 -0
  125. package/dist/src/semantics/repos.js +390 -0
  126. package/dist/src/semantics/rulesets.d.ts +2 -0
  127. package/dist/src/semantics/rulesets.js +111 -0
  128. package/dist/src/semantics/search.d.ts +2 -0
  129. package/dist/src/semantics/search.js +84 -0
  130. package/dist/src/semantics/security.d.ts +2 -0
  131. package/dist/src/semantics/security.js +177 -0
  132. package/dist/src/semantics/shared.d.ts +61 -0
  133. package/dist/src/semantics/shared.js +151 -0
  134. package/dist/src/semantics/users.d.ts +2 -0
  135. package/dist/src/semantics/users.js +255 -0
  136. package/dist/src/semantics/webhooks.d.ts +2 -0
  137. package/dist/src/semantics/webhooks.js +107 -0
  138. package/dist/test-fixtures/github-a11y-reference.pr-list.SOURCE.md +56 -0
  139. package/dist/test-fixtures/github-a11y-reference.pr-list.json +1447 -0
  140. package/dist/test-fixtures/github-comment-schema.SOURCE.md +11 -0
  141. package/dist/test-fixtures/github-comment-schema.json +702 -0
  142. package/dist/test-fixtures/github-known-deviations.json +94 -0
  143. package/dist/test-fixtures/github-openapi-operations.SOURCE.md +76 -0
  144. package/dist/test-fixtures/github-openapi-operations.json +362 -0
  145. package/dist/test-fixtures/github-pull-schema.SOURCE.md +37 -0
  146. package/dist/test-fixtures/github-pull-schema.json +3601 -0
  147. package/dist/test-fixtures/github-review-schema.SOURCE.md +12 -0
  148. package/dist/test-fixtures/github-review-schema.json +236 -0
  149. package/package.json +20 -11
  150. package/src/cli.ts +7 -6
  151. package/src/generated/graphql-sdl.gen.json +1 -0
  152. package/src/generated/graphql.gen.json +1 -0
  153. package/src/generated/surface.gen.json +1 -0
  154. package/src/generated/ui.gen.json +1 -0
  155. package/src/github-a11y-snapshot.uitest.ts +5 -5
  156. package/src/github-budget.ts +4 -4
  157. package/src/github-capabilities.ts +1993 -556
  158. package/src/github-conformance.ts +12 -7
  159. package/src/github-connector.ts +108 -96
  160. package/src/github-events.ts +225 -95
  161. package/src/github-git-http.ts +48 -84
  162. package/src/github-git-plane.ts +247 -385
  163. package/src/github-journey.uitest.ts +28 -44
  164. package/src/github-mirror-state.ts +23 -61
  165. package/src/github-mirror-ui.ts +16 -10
  166. package/src/github-server.ts +158 -60
  167. package/src/github-shared.ts +1 -1
  168. package/src/github-twin.ts +1525 -4494
  169. package/src/github-ui-conformance.ts +7 -8
  170. package/src/github-ui-structure.ts +19 -18
  171. package/src/graphql-wire.ts +115 -0
  172. package/src/index.ts +50 -7
  173. package/src/manifest.ts +605 -0
  174. package/src/npm-registry.ts +43 -0
  175. package/src/screens/app-installation.tsx +217 -0
  176. package/src/screens/app-manifest.tsx +97 -0
  177. package/src/screens/oauth.tsx +258 -0
  178. package/src/screens/session.tsx +193 -0
  179. package/src/semantics/actions.ts +395 -0
  180. package/src/semantics/activity.ts +171 -0
  181. package/src/semantics/apps.ts +133 -0
  182. package/src/semantics/branches.ts +133 -0
  183. package/src/semantics/checks.ts +163 -0
  184. package/src/semantics/code-scanning-upload.ts +92 -0
  185. package/src/semantics/codespaces.ts +58 -0
  186. package/src/semantics/commits.ts +109 -0
  187. package/src/semantics/contents.ts +112 -0
  188. package/src/semantics/deployments.ts +116 -0
  189. package/src/semantics/gists.ts +85 -0
  190. package/src/semantics/git.ts +226 -0
  191. package/src/semantics/graphql.ts +505 -0
  192. package/src/semantics/index.ts +68 -0
  193. package/src/semantics/issues.ts +434 -0
  194. package/src/semantics/keys.ts +66 -0
  195. package/src/semantics/labels.ts +91 -0
  196. package/src/semantics/meta.ts +139 -0
  197. package/src/semantics/notifications.ts +58 -0
  198. package/src/semantics/orgs.ts +383 -0
  199. package/src/semantics/packages.ts +59 -0
  200. package/src/semantics/pages.ts +154 -0
  201. package/src/semantics/projects.ts +246 -0
  202. package/src/semantics/pulls.ts +421 -0
  203. package/src/semantics/push-reactions.ts +471 -0
  204. package/src/semantics/releases.ts +213 -0
  205. package/src/semantics/repo-invitations.ts +112 -0
  206. package/src/semantics/repos.ts +373 -0
  207. package/src/semantics/rulesets.ts +107 -0
  208. package/src/semantics/search.ts +83 -0
  209. package/src/semantics/security.ts +153 -0
  210. package/src/semantics/shared.ts +181 -0
  211. package/src/semantics/users.ts +252 -0
  212. package/src/semantics/webhooks.ts +101 -0
  213. package/test-fixtures/github-known-deviations.json +7 -8
  214. package/test-fixtures/github-openapi-operations.json +46 -227
  215. package/src/github-graphql.ts +0 -398
@@ -14,7 +14,7 @@
14
14
  // so those are MODELED features and the mirror renders them (empty for observed-only
15
15
  // PRs, preserving honesty). What stays 'unmodeled' is what the twin models for NEITHER
16
16
  // observed nor local writes: PR author identity and the head branch NAME.
17
- import { checkUiCompleteness, type UiSurface, type UiCompletenessReport } from '@volter/twin-tooling';
17
+ import { checkUiCompleteness, type UiSurface, type UiCompletenessReport } from '@volter/world-tooling';
18
18
 
19
19
  /** What real GitHub shows on the PR list, PR conversation, and issues list — classified vs the twin. */
20
20
  export const GITHUB_UI_INVENTORY: UiSurface[] = [
@@ -40,11 +40,10 @@ export const GITHUB_UI_INVENTORY: UiSurface[] = [
40
40
  { key: 'prUrl', label: 'Link to PR on GitHub', status: 'rendered' },
41
41
  { key: 'issuesList', label: 'Issues list (first-class issues view)', status: 'rendered' },
42
42
  { key: 'repoSelector', label: 'Per-repo selector (repo grouping)', status: 'rendered' },
43
- // reviewCommentsCount is computed + emitted in the mirror store and now rendered in the
44
- // conversation header alongside reviewCount + commentsCount.
43
+ // reviewCommentsCount counts into the pull request's Conversation tab, with its comments and reviews.
45
44
  { key: 'reviewCommentsCount', label: 'Review-comment (diff-thread) count', status: 'rendered' },
46
45
  // CI rollup (commit statuses + check-runs) over the PR head_sha — the twin now models
47
- // statuses[]/checkRuns[]; the mirror renders a ✓/✗/● indicator + a Checks side-section.
46
+ // statuses[]/checkRuns[]; the mirror renders a ✓/✗/● indicator, the merge box's checks and a Checks tab.
48
47
  { key: 'checksStatus', label: 'CI / status checks', status: 'rendered' },
49
48
  // milestone: the twin models milestone NUMBER + state.milestones; the mirror renders
50
49
  // the milestone title as a badge on PR/issue rows + a Milestone side-section.
@@ -53,17 +52,17 @@ export const GITHUB_UI_INVENTORY: UiSurface[] = [
53
52
  // cross-reference links on the issue rows.
54
53
  { key: 'linkedIssues', label: 'Linked / source issue', status: 'rendered' },
55
54
  // requested reviewers (distinct from submitted reviews): the twin models
56
- // requested_reviewers[]; the mirror renders them as chips in the Reviewers side-section.
55
+ // requested_reviewers[]; the mirror lists them, awaiting review, in the Reviewers sidebar section.
57
56
  { key: 'requestedReviewers', label: 'Requested reviewers', status: 'rendered' },
58
57
  // per-file diffs: the twin models pr.files (GithubPullFile) on LOCAL writes; the mirror
59
- // renders the conversation's Files-changed section (filename + status + +/- counts).
58
+ // renders the Files changed tab (each file's path, +/- counts and unified diff).
60
59
  // Empty for observed-only PRs (no per-file evidence) — honesty preserved.
61
60
  { key: 'filesChangedContent', label: 'Per-file diff list (filenames + status + counts)', status: 'rendered' },
62
61
  // additions/deletions: the twin rolls these up from the carried file list on LOCAL writes;
63
- // the mirror renders them in the Aside snapshot counts. Undefined for observed-only PRs.
62
+ // the mirror renders them as the pull request's diffstat. Undefined for observed-only PRs.
64
63
  { key: 'diffStats', label: 'Additions / deletions line counts', status: 'rendered' },
65
64
  // per-commit detail: the twin models pr.commits_list (GithubPullCommit) on LOCAL writes;
66
- // the mirror renders the conversation's Commits section (sha + message + author + date).
65
+ // the mirror renders them in the timeline and the Commits tab (sha + message + author + date).
67
66
  // Empty for observed-only PRs (only a commit COUNT is observed) — honesty preserved.
68
67
  { key: 'commitsContent', label: 'Per-commit list (sha + message + author)', status: 'rendered' },
69
68
 
@@ -1,7 +1,7 @@
1
1
  // RUNG-5 STRUCTURAL DOM CHECKLIST for the GitHub mirror — the PROXIMITY half of the
2
2
  // UI rung. Completeness asks "does the mirror show the data?"; this asks "does the
3
3
  // mirror's rendered DOM look structurally like the real GitHub UI?" — the PR table
4
- // emits a row per PR, each row carries a state badge, the conversation pane has the
4
+ // emits a row per PR, each row carries its state icon, the conversation pane has the
5
5
  // reviews/comments timeline, the Aside carries the reviewers/labels/assignees/milestone
6
6
  // sections, a CI status element renders, the issues view emits a row per issue.
7
7
  //
@@ -11,9 +11,9 @@
11
11
  // inventory: the real React render.
12
12
  import { renderToStaticMarkup } from 'react-dom/server';
13
13
  import { createElement } from 'react';
14
- import { checkUiStructure, type UiStructureCheck, type UiStructureReport } from '@volter/twin-tooling';
14
+ import { checkUiStructure, type UiStructureCheck, type UiStructureReport } from '@volter/world-tooling';
15
15
  import {
16
- PrList, Conversation, Aside, IssuesTable, CiStatus, ActionsView, ReleasesView, DiscussionsView, DeploymentsView,
16
+ PrList, Conversation, Aside, FilesDiff, IssuesTable, CiStatus, ActionsView, ReleasesView, DiscussionsView, DeploymentsView,
17
17
  type PullRequest, type Issue, type Review, type Commit, type Comment, type ChangedFile,
18
18
  type WorkflowRun, type Job, type Release, type Discussion, type Deployment, type Environment,
19
19
  } from '../client/github-mirror.tsx';
@@ -47,7 +47,7 @@ const REVIEWS: Review[] = [{ repo: 'o/r', number: 1, reviewer: 'carol', state: '
47
47
  const COMMITS: Commit[] = [{ repo: 'o/r', number: 1, sha: 'abcdef1234567', message: 'do the thing', authorLogin: 'octocat', date: '2026-04-01T12:00:00Z' }];
48
48
  const COMMENTS: Comment[] = [{ repo: 'o/r', number: 1, author: 'dave', body: 'a note', createdAt: '2026-04-02T02:00:00Z' }];
49
49
  const REVIEW_COMMENTS: Comment[] = [{ repo: 'o/r', number: 1, author: 'erin', body: 'inline diff note', path: 'src/x.ts', createdAt: '2026-04-02T03:00:00Z' }];
50
- const FILES: ChangedFile[] = [{ repo: 'o/r', number: 1, filename: 'src/x.ts', status: 'modified', additions: 42, deletions: 7 }];
50
+ const FILES: ChangedFile[] = [{ repo: 'o/r', number: 1, filename: 'src/x.ts', status: 'modified', additions: 1, deletions: 1, patch: '@@ -1,2 +1,2 @@\n-old line\n+new line\n context' }];
51
51
  // Actions: two runs (one completed/success, one cancelled) so the runs list has >1 row, plus
52
52
  // a job (with steps) for the first run so the run-detail jobs section renders.
53
53
  const RUNS: WorkflowRun[] = [
@@ -109,6 +109,7 @@ export function renderGithubMirrorMarkup() {
109
109
  prList: renderToStaticMarkup(createElement(PrList, { prs: [PR, PR_TWO], reviews: REVIEWS, selectedKey: 'o/r#1' })),
110
110
  conversation: renderToStaticMarkup(createElement(Conversation, { pr: PR, reviews: REVIEWS, commits: COMMITS, comments: COMMENTS, reviewComments: REVIEW_COMMENTS, files: FILES })),
111
111
  aside: renderToStaticMarkup(createElement(Aside, { pr: PR, reviews: REVIEWS, commits: COMMITS, files: FILES })),
112
+ files: renderToStaticMarkup(createElement(FilesDiff, { files: FILES })),
112
113
  issues: renderToStaticMarkup(createElement(IssuesTable, { issues: [ISSUE, ISSUE_TWO] })),
113
114
  ciStatus: renderToStaticMarkup(createElement(CiStatus, { status: 'success' })),
114
115
  actions: renderToStaticMarkup(createElement(ActionsView, { runs: RUNS, jobs: JOBS, selectedRunId: 101 })),
@@ -125,12 +126,12 @@ export function githubUiStructureChecks(): UiStructureCheck[] {
125
126
  {
126
127
  key: 'pr-list-row-per-pr',
127
128
  label: 'the PR list emits a row (.pr-row) per pull request',
128
- present: count(dom.prList, 'class="pr-row') === 2,
129
+ present: count(dom.prList, 'class="pr-row"') === 2,
129
130
  },
130
131
  {
131
- key: 'pr-row-state-badge',
132
- label: 'each PR row shows a state badge element (.state-badge)',
133
- present: count(dom.prList, 'class="state-badge') >= 2,
132
+ key: 'pr-row-state-icon',
133
+ label: 'each PR row shows its open/merged/closed state icon (.pr-state-icon)',
134
+ present: count(dom.prList, 'class="pr-state-icon') === 2,
134
135
  },
135
136
  {
136
137
  key: 'pr-row-ci-status',
@@ -139,38 +140,38 @@ export function githubUiStructureChecks(): UiStructureCheck[] {
139
140
  },
140
141
  {
141
142
  key: 'conversation-detail-pane',
142
- label: 'the conversation/detail pane renders (section.conversation with a heading + state line)',
143
- present: dom.conversation.includes('class="conversation"') && dom.conversation.includes('class="state-line"'),
143
+ label: 'the conversation pane renders (section.conversation with the merge box)',
144
+ present: dom.conversation.includes('class="conversation') && dom.conversation.includes('class="gh-mergebox"'),
144
145
  },
145
146
  {
146
147
  key: 'conversation-comments-timeline',
147
148
  label: 'the conversation pane has a comments timeline (.timeline-item with a .comment)',
148
- present: dom.conversation.includes('class="timeline-item"') && dom.conversation.includes('class="comment"'),
149
+ present: dom.conversation.includes('class="timeline-item') && dom.conversation.includes('class="comment'),
149
150
  },
150
151
  {
151
152
  key: 'conversation-reviews-section',
152
153
  label: 'the conversation pane has a reviews section (.review-card)',
153
- present: dom.conversation.includes('class="review-card"'),
154
+ present: dom.conversation.includes('class="review-card'),
154
155
  },
155
156
  {
156
- key: 'conversation-files-changed',
157
- label: 'the conversation pane has a files-changed section (.file-list with a .file-row)',
158
- present: dom.conversation.includes('class="file-list"') && dom.conversation.includes('class="file-row"'),
157
+ key: 'files-changed-diff',
158
+ label: 'the Files changed tab renders each file with its diff rows (.gh-file with .gh-diff-add and .gh-diff-del)',
159
+ present: dom.files.includes('class="diff-file gh-file"') && dom.files.includes('class="gh-diff-add"') && dom.files.includes('class="gh-diff-del"'),
159
160
  },
160
161
  {
161
162
  key: 'aside-sidebar',
162
163
  label: 'the detail Aside renders as a sidebar landmark (aside.sidebar)',
163
- present: dom.aside.includes('<aside class="sidebar"'),
164
+ present: dom.aside.includes('<aside class="sidebar'),
164
165
  },
165
166
  {
166
167
  key: 'aside-reviewers-section',
167
168
  label: 'the Aside has a Reviewers section',
168
- present: dom.aside.includes('<h3>Reviewers</h3>'),
169
+ present: dom.aside.includes('<h3>Reviewers<'),
169
170
  },
170
171
  {
171
172
  key: 'aside-labels-assignees-milestone',
172
173
  label: 'the Aside has Labels, Assignees and Milestone sections',
173
- present: dom.aside.includes('<h3>Labels</h3>') && dom.aside.includes('<h3>Assignees</h3>') && dom.aside.includes('<h3>Milestone</h3>'),
174
+ present: dom.aside.includes('<h3>Labels<') && dom.aside.includes('<h3>Assignees<') && dom.aside.includes('<h3>Milestone<'),
174
175
  },
175
176
  {
176
177
  key: 'ci-status-element',
@@ -0,0 +1,115 @@
1
+ // THE GRAPHQL WIRE — a vendor's GraphQL API served from its published schema over the pack's state
2
+ // (docs/contributing/architecture.md, "Protocol 3"). A query is parsed and validated against
3
+ // the vendor's own schema, so an unknown field is the GraphQL error the vendor gives. A field
4
+ // resolves from its resolver when the pack writes one, else from the stored object: the field's own
5
+ // name, its snake_case form, or the pack's alias for it. A list answering a `...Connection` field is
6
+ // wrapped as a Relay connection, and an enum resolves case-insensitively. The wire takes a query
7
+ // only when every field it selects is modeled; any other query is answered by the pack's `unmodeled`,
8
+ // naming the first field nothing models. An error a resolver raises carries its `extensions.type` as
9
+ // the error's `type`, as GitHub's GraphQL errors do.
10
+ // Nothing here knows a vendor. It needs the `graphql` protocol library, which the kernel may not
11
+ // carry, so it lives beside the packs that use it.
12
+ import {
13
+ execute,
14
+ getNamedType,
15
+ GraphQLError,
16
+ isEnumType,
17
+ isObjectType,
18
+ parse,
19
+ TypeInfo,
20
+ validate,
21
+ visit,
22
+ visitWithTypeInfo,
23
+ buildSchema,
24
+ type DocumentNode,
25
+ type GraphQLFieldResolver,
26
+ type GraphQLSchema,
27
+ } from 'graphql';
28
+
29
+ export type GraphqlContext = Record<string, unknown> & { request: Request };
30
+ export type FieldResolver = (source: Record<string, unknown>, args: Record<string, unknown>, context: GraphqlContext) => unknown;
31
+
32
+ export type GraphqlWireOptions = {
33
+ /** the vendor's schema, loaded on the first GraphQL request */
34
+ loadSdl: () => Promise<string>;
35
+ /** resolvers by `Type.field` (`Query.repository`, `PullRequest.state`) */
36
+ resolvers: Record<string, FieldResolver>;
37
+ /** per type, a GraphQL field name → the stored field that holds it (`nameWithOwner` → `full_name`) */
38
+ aliases?: Record<string, Record<string, string>>;
39
+ /** per type, the stored fields the default resolver may read as they are */
40
+ stored?: Record<string, string[]>;
41
+ /** what every resolver of one request receives beside the request (a pack's handler interface) */
42
+ context: (request: Request) => Record<string, unknown> | Promise<Record<string, unknown>>;
43
+ /** the answer to a query that selects a field nothing models: the first such field and its type */
44
+ unmodeled: (field: { type: string; field: string }) => Response;
45
+ };
46
+
47
+ const snake = (s: string): string => s.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
48
+
49
+ function connection(items: unknown[], args: Record<string, unknown>): Record<string, unknown> {
50
+ const first = typeof args.first === 'number' ? args.first : undefined;
51
+ const last = typeof args.last === 'number' ? args.last : undefined;
52
+ const after = typeof args.after === 'string' ? Number(atob(args.after)) + 1 : 0;
53
+ let slice = items.slice(after);
54
+ if (first !== undefined) slice = slice.slice(0, first);
55
+ if (last !== undefined) slice = slice.slice(-last);
56
+ const start = after + (last !== undefined ? Math.max(0, items.slice(after).length - last) : 0);
57
+ const cursor = (i: number): string => btoa(String(start + i));
58
+ return {
59
+ totalCount: items.length,
60
+ nodes: slice,
61
+ edges: slice.map((node, i) => ({ node, cursor: cursor(i) })),
62
+ pageInfo: { hasNextPage: start + slice.length < items.length, hasPreviousPage: start > 0, startCursor: slice.length ? cursor(0) : null, endCursor: slice.length ? cursor(slice.length - 1) : null },
63
+ };
64
+ }
65
+
66
+ /** A query GitHub cannot parse: its syntax error, as GraphQL answers it. */
67
+ function syntaxError(error: unknown): Response {
68
+ return Response.json({ errors: [{ message: error instanceof GraphQLError ? error.message : String(error) }] });
69
+ }
70
+
71
+ export function createGraphqlWire(options: GraphqlWireOptions): (request: Request) => Promise<Response> {
72
+ let schema: GraphQLSchema | undefined;
73
+ const modeled = (type: string, field: string): boolean =>
74
+ field === '__typename' ||
75
+ `${type}.${field}` in options.resolvers ||
76
+ !!options.aliases?.[type]?.[field] ||
77
+ !!options.stored?.[type]?.includes(field) ||
78
+ !!options.stored?.[type]?.includes(snake(field));
79
+ const fieldResolver: GraphQLFieldResolver<unknown, GraphqlContext> = (source, args, context, info) => {
80
+ const own = options.resolvers[`${info.parentType.name}.${info.fieldName}`];
81
+ const obj = (source ?? {}) as Record<string, unknown>;
82
+ let value = own ? own(obj, args, context) : (obj[options.aliases?.[info.parentType.name]?.[info.fieldName] ?? info.fieldName] ?? obj[snake(info.fieldName)] ?? null);
83
+ const named = getNamedType(info.returnType);
84
+ if (Array.isArray(value) && isObjectType(named) && named.name.endsWith('Connection')) value = connection(value, args);
85
+ if (typeof value === 'string' && isEnumType(named)) value = named.getValues().find((v) => v.name.toLowerCase() === value!.toString().toLowerCase())?.name ?? value;
86
+ return value;
87
+ };
88
+ return async (request) => {
89
+ schema ??= buildSchema(await options.loadSdl(), { assumeValidSDL: true });
90
+ const payload = (await request.clone().json().catch(() => ({}))) as { query?: string; variables?: Record<string, unknown>; operationName?: string };
91
+ let document: DocumentNode;
92
+ try { document = parse(payload.query ?? ''); } catch (error) { return syntaxError(error); }
93
+ const invalid = validate(schema, document);
94
+ if (invalid.length) return Response.json({ errors: invalid.map((e) => ({ message: e.message, locations: e.locations })) });
95
+ // every selected field modeled, or the existing resolver answers
96
+ const typeInfo = new TypeInfo(schema);
97
+ let unmodeled: { type: string; field: string } | undefined;
98
+ visit(document, visitWithTypeInfo(typeInfo, {
99
+ Field(node) {
100
+ const parent = typeInfo.getParentType();
101
+ if (!unmodeled && parent && isObjectType(parent) && !parent.name.endsWith('Connection') && !parent.name.endsWith('Edge') && parent.name !== 'PageInfo' && !modeled(parent.name, node.name.value)) unmodeled = { type: parent.name, field: node.name.value };
102
+ },
103
+ }));
104
+ if (unmodeled) return options.unmodeled(unmodeled);
105
+ const result = await execute({
106
+ schema,
107
+ document,
108
+ variableValues: payload.variables ?? {},
109
+ ...(payload.operationName ? { operationName: payload.operationName } : {}),
110
+ contextValue: { ...(await options.context(request)), request },
111
+ fieldResolver,
112
+ });
113
+ return Response.json({ ...(result.data !== undefined ? { data: result.data } : {}), ...(result.errors?.length ? { errors: result.errors.map((e) => ({ ...(typeof e.extensions?.type === 'string' ? { type: e.extensions.type } : {}), message: e.message, path: e.path })) } : {}) });
114
+ };
115
+ }
package/src/index.ts CHANGED
@@ -11,7 +11,7 @@ export { handleGithubGitSmartHttp, parseGitSmartHttpPath, reconcileGitPlaneRefs
11
11
  export type { GitSmartHttpResponse, GitSmartHttpRoute } from './github-git-http.ts';
12
12
  export type { GithubComment, GithubIssue, GithubPr, GithubResponse, GithubReview, GithubState, GithubWriteEvent, GithubWriteOutcome } from './github-twin.ts';
13
13
  export { createGithubTwinFetch, createGithubTwinServer } from './github-server.ts';
14
- export { clearGithubWebhooks, emitGithubEvent, listGithubWebhooks, registerGithubWebhook } from './github-events.ts';
14
+ export { emitGithubEvent } from './github-events.ts';
15
15
  export type { GithubWebhook, GithubWebhookDelivery } from './github-events.ts';
16
16
  // The mirror is a pure frontend (R3): shell + assets + the mounted adapter. Its state comes
17
17
  // from the twin's store door, whose projection lives server-side in github-mirror-state.ts.
@@ -19,8 +19,8 @@ export { buildGithubMirrorClient, createGithubMirrorServer, githubMirrorHtml } f
19
19
  export { githubMirrorState } from './github-mirror-state.ts';
20
20
  export { GITHUB_MIRROR_STORE, GITHUB_MIRROR_STORE_PATH, GITHUB_WEB_ORIGIN, githubIssuePath, githubPullPath } from './github-shared.ts';
21
21
  export {
22
- liveGithubExecute, observedPrsToResources, pullGithubPrs, pushGithubAction,
23
- pushGithubToRemote, pushPendingGithubActions, syncGithubFromReal, syncGithubFromRemote,
22
+ githubPushExecutor, liveGithubExecute, observedPrsToResources, performGithubAction, pullGithubPrs, pushGithubAction,
23
+ syncGithubFromReal, syncGithubFromRemote,
24
24
  } from './github-connector.ts';
25
25
  export type { GithubExecute, LiveGithubOptions } from './github-connector.ts';
26
26
  // The client-side rate budget — the fail-closed backstop `liveGithubExecute` routes every live
@@ -43,10 +43,31 @@ export {
43
43
  export type { GithubBudgetErrorKind, GithubBudgetOptions, GithubBudgetReservation, GithubBudgetSnapshot } from './github-budget.ts';
44
44
 
45
45
  // Registry descriptor (#1).
46
- import type { TwinPack } from '@volter/twin';
46
+ import type { TwinPack } from '@volter/world-core';
47
+ import { registerPack } from '@volter/world-core';
47
48
  import { GITHUB_RATE_BUDGET as RATE_BUDGET } from './github-budget.ts';
49
+ import { performGithubAction, syncGithubFromRemote } from './github-connector.ts';
48
50
  export const pack: TwinPack = {
49
51
  vendor: 'github',
52
+ // PROTOCOL 2 (docs/contributing/architecture.md#protocol-2-the-pack-is-a-plugin): the pack is a plugin — its wire, its tree, and its half of
53
+ // the real state system: perform one entry against GitHub, refresh the root from it.
54
+ protocol: '2',
55
+ refresh: { every: '5m', webhook: true, onDemand: { atMost: '30s' } },
56
+ stateSystem: { perform: performGithubAction, refresh: syncGithubFromRemote },
57
+ roundTrip: { method: 'POST', path: '/repos/acme/web/issues', body: { title: 'round trip' }, headers: { authorization: 'token round-trip' } },
58
+ // docs/contributing/architecture.md#alias-aware-lookup-at-the-request-boundary: a comment and a review name their issue or PR by `repository` + `number`
59
+ // (an issue and a PR share the `owner/repo#issue:number` id space); when the head adopts GitHub's
60
+ // number for the parent, the kernel resolves these before the comment is performed.
61
+ // shape parity refreshes the twin from itself: github's refresh needs the repo on its origin
62
+ parityOrigin: 'http://twin/repos/acme/web',
63
+ shapeParity: 'held',
64
+ referenceTrip: { method: 'POST', path: '/repos/acme/web/issues/{{number}}/comments', body: { body: 'a comment on the round trip' }, headers: { authorization: 'token round-trip' } },
65
+ references: (['issue_comment', 'review'] as const).map((type) => ({
66
+ type, to: 'issue',
67
+ key: (f: Record<string, unknown>) => (f.repository === undefined || f.number === undefined ? undefined : `${String(f.repository)}#issue:${String(f.number)}`),
68
+ adopt: (_f: Record<string, unknown>, vendorId: string) => ({ number: Number(vendorId.split('#issue:').pop()) }),
69
+ })),
70
+ engine: { module: 'github-git-plane', note: 'real bare repositories: git objects live in the plane, the tree references them by sha' },
50
71
  // The SAME object github-budget.ts declares at module load — one source of truth, so registering
51
72
  // the pack and importing the connector can never arm two different ceilings.
52
73
  rateBudget: RATE_BUDGET,
@@ -56,10 +77,10 @@ export const pack: TwinPack = {
56
77
  transport: 'rest',
57
78
  archetype: 'engine-control',
58
79
  bin: 'world-github',
59
- resources: ['pull_request', 'pull_request_review', 'pull_request_review_comment', 'issue', 'issue_comment'],
80
+ resources: ['pull_request', 'pull_request_review', 'pull_request_review_comment', 'issue', 'issue_comment', 'repository', 'branch', 'git_ref'],
60
81
  specSource: 'test-fixtures/github-pull-schema.json (full GitHub OpenAPI PR schema)',
61
82
  description: 'GitHub PR-evidence REST twin — writes, webhooks, UI mirror.',
62
- // Adoption + interception, all in the pack's one home (TWIN-PACK-CONTRACT back-migration
83
+ // Adoption + interception, all in the pack's one home (descriptor-first back-migration, adding-a-twin.md §3,
63
84
  // 2026-08-31; the eight SDK names, the `@octokit/` scope and the bare `github`/`gh` stems moved
64
85
  // off the central maps unchanged). The two canonical Octokit clients plus the official
65
86
  // auth/webhook companions — each battery-observed as a repo's ONLY GitHub dep — and the whole
@@ -85,5 +106,27 @@ export const pack: TwinPack = {
85
106
  'APPSMITHOAUTH2GITHUB',
86
107
  ],
87
108
  },
88
- hosts: [{ host: 'api.github.com' }],
109
+ // The REST API, and on github.com git's smart-HTTP paths (github-git-http.ts serves them: an unmodified
110
+ // `git clone https://github.com/o/r.git` lands in the twin) and the OAuth web flow (src/screens/oauth.tsx: an
111
+ // application's unmodified sign-in with GitHub), a GitHub App's installation page (src/screens/app-installation.tsx), the
112
+ // page that registers an app from its manifest (src/screens/app-manifest.tsx) and GitHub Packages' npm registry (src/npm-registry.ts), where `npm publish` lands;
113
+ // and on uploads.github.com a release asset's upload.
114
+ // Every other github.com page stays the real site's.
115
+ hosts: [
116
+ { host: 'api.github.com' },
117
+ // a release's upload_url: its assets are uploaded here (docs.github.com/rest/releases/assets#upload-a-release-asset)
118
+ { host: 'uploads.github.com', pathPattern: '^/repos/[^/]+/[^/]+/releases/\\d+/assets$' },
119
+ { host: 'github.com', pathPattern: '^/[^/]+/[^/]+/(info/refs|git-upload-pack|git-receive-pack)$' },
120
+ { host: 'github.com', pathPattern: '^/login/oauth/(authorize|access_token)$' },
121
+ // the sign-in the consent and the other pages send a visitor who is not signed in to, and its two-factor step
122
+ // (src/screens/session.tsx); without these a browser following the redirect leaves the World
123
+ { host: 'github.com', pathPattern: '^/(login|session|sessions/two-factor(/app)?)$' },
124
+ { host: 'github.com', pathPattern: '^/apps/[^/]+/installations/new$' },
125
+ { host: 'github.com', pathPattern: '^/(organizations/[^/]+/)?settings/apps/new$' },
126
+ { host: 'npm.pkg.github.com', pathPattern: '^/@' },
127
+ ],
89
128
  };
129
+
130
+ // PROTOCOL 2: the pack registers itself on load — its rate budget and its state system (the head
131
+ // resolves `stateSystemFor(vendor)` in the process that serves the twin).
132
+ registerPack(pack);