@volter/twin-github 0.1.2 → 2.0.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 +88 -36
- package/client/github-mirror.css +277 -319
- package/client/github-mirror.d.ts +418 -0
- package/client/github-mirror.js +485 -0
- package/client/github-mirror.tsx +153 -357
- package/client/pulls-rest.ts +159 -0
- package/client/pulls-workspace.tsx +841 -0
- package/dist/client/github-mirror.bundle.js +239 -0
- package/dist/client/github-mirror.css +916 -0
- package/dist/client/github-mirror.d.ts +418 -0
- package/dist/client/github-mirror.js +485 -0
- package/dist/client/github-mirror.tsx +1315 -0
- package/dist/client/pulls-rest.d.ts +42 -0
- package/dist/client/pulls-rest.js +140 -0
- package/dist/client/pulls-rest.ts +159 -0
- package/dist/client/pulls-workspace.bundle.js +22 -0
- package/dist/client/pulls-workspace.d.ts +114 -0
- package/dist/client/pulls-workspace.js +418 -0
- package/dist/client/pulls-workspace.tsx +841 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +40 -0
- package/dist/src/generated/graphql-sdl.gen.json +1 -0
- package/dist/src/generated/graphql.gen.json +1 -0
- package/dist/src/generated/surface.gen.json +1 -0
- package/dist/src/generated/ui.gen.json +1 -0
- package/dist/src/github-budget.d.ts +69 -0
- package/dist/src/github-budget.js +172 -0
- package/dist/src/github-capabilities.d.ts +5 -0
- package/dist/src/github-capabilities.js +4468 -0
- package/dist/src/github-conformance.d.ts +43 -0
- package/dist/src/github-conformance.js +76 -0
- package/dist/src/github-connector.d.ts +307 -0
- package/dist/src/github-connector.js +1398 -0
- package/dist/src/github-events.d.ts +41 -0
- package/dist/src/github-events.js +230 -0
- package/dist/src/github-git-http.d.ts +49 -0
- package/dist/src/github-git-http.js +185 -0
- package/dist/src/github-git-plane.d.ts +114 -0
- package/dist/src/github-git-plane.js +407 -0
- package/dist/src/github-mirror-state.d.ts +2 -0
- package/dist/src/github-mirror-state.js +335 -0
- package/dist/src/github-mirror-ui.d.ts +20 -0
- package/dist/src/github-mirror-ui.js +101 -0
- package/dist/src/github-server.d.ts +14 -0
- package/dist/src/github-server.js +240 -0
- package/dist/src/github-shared.d.ts +12 -0
- package/dist/src/github-shared.js +21 -0
- package/dist/src/github-twin.d.ts +1411 -0
- package/dist/src/github-twin.js +4084 -0
- package/dist/src/github-ui-conformance.d.ts +4 -0
- package/dist/src/github-ui-conformance.js +105 -0
- package/dist/src/github-ui-structure.d.ts +18 -0
- package/dist/src/github-ui-structure.js +251 -0
- package/dist/src/graphql-wire.d.ts +22 -0
- package/dist/src/graphql-wire.js +89 -0
- package/dist/src/index.d.ts +17 -0
- package/dist/src/index.js +101 -0
- package/dist/src/manifest.d.ts +8 -0
- package/dist/src/manifest.js +597 -0
- package/dist/src/npm-registry.d.ts +7 -0
- package/dist/src/npm-registry.js +47 -0
- package/dist/src/screens/app-installation.d.ts +3 -0
- package/dist/src/screens/app-installation.js +166 -0
- package/dist/src/screens/app-manifest.d.ts +3 -0
- package/dist/src/screens/app-manifest.js +81 -0
- package/dist/src/screens/oauth.d.ts +15 -0
- package/dist/src/screens/oauth.js +257 -0
- package/dist/src/screens/session.d.ts +8 -0
- package/dist/src/screens/session.js +159 -0
- package/dist/src/semantics/actions.d.ts +2 -0
- package/dist/src/semantics/actions.js +413 -0
- package/dist/src/semantics/activity.d.ts +4 -0
- package/dist/src/semantics/activity.js +161 -0
- package/dist/src/semantics/apps.d.ts +2 -0
- package/dist/src/semantics/apps.js +144 -0
- package/dist/src/semantics/branches.d.ts +2 -0
- package/dist/src/semantics/branches.js +136 -0
- package/dist/src/semantics/checks.d.ts +2 -0
- package/dist/src/semantics/checks.js +176 -0
- package/dist/src/semantics/code-scanning-upload.d.ts +2 -0
- package/dist/src/semantics/code-scanning-upload.js +97 -0
- package/dist/src/semantics/codespaces.d.ts +2 -0
- package/dist/src/semantics/codespaces.js +58 -0
- package/dist/src/semantics/commits.d.ts +2 -0
- package/dist/src/semantics/commits.js +109 -0
- package/dist/src/semantics/contents.d.ts +2 -0
- package/dist/src/semantics/contents.js +131 -0
- package/dist/src/semantics/deployments.d.ts +2 -0
- package/dist/src/semantics/deployments.js +127 -0
- package/dist/src/semantics/gists.d.ts +2 -0
- package/dist/src/semantics/gists.js +86 -0
- package/dist/src/semantics/git.d.ts +2 -0
- package/dist/src/semantics/git.js +235 -0
- package/dist/src/semantics/graphql.d.ts +7 -0
- package/dist/src/semantics/graphql.js +512 -0
- package/dist/src/semantics/index.d.ts +5 -0
- package/dist/src/semantics/index.js +62 -0
- package/dist/src/semantics/issues.d.ts +2 -0
- package/dist/src/semantics/issues.js +456 -0
- package/dist/src/semantics/keys.d.ts +2 -0
- package/dist/src/semantics/keys.js +66 -0
- package/dist/src/semantics/labels.d.ts +2 -0
- package/dist/src/semantics/labels.js +100 -0
- package/dist/src/semantics/meta.d.ts +12 -0
- package/dist/src/semantics/meta.js +144 -0
- package/dist/src/semantics/notifications.d.ts +4 -0
- package/dist/src/semantics/notifications.js +62 -0
- package/dist/src/semantics/orgs.d.ts +2 -0
- package/dist/src/semantics/orgs.js +420 -0
- package/dist/src/semantics/packages.d.ts +2 -0
- package/dist/src/semantics/packages.js +55 -0
- package/dist/src/semantics/pages.d.ts +2 -0
- package/dist/src/semantics/pages.js +176 -0
- package/dist/src/semantics/projects.d.ts +2 -0
- package/dist/src/semantics/projects.js +239 -0
- package/dist/src/semantics/pulls.d.ts +2 -0
- package/dist/src/semantics/pulls.js +462 -0
- package/dist/src/semantics/push-reactions.d.ts +101 -0
- package/dist/src/semantics/push-reactions.js +513 -0
- package/dist/src/semantics/releases.d.ts +19 -0
- package/dist/src/semantics/releases.js +231 -0
- package/dist/src/semantics/repo-invitations.d.ts +2 -0
- package/dist/src/semantics/repo-invitations.js +98 -0
- package/dist/src/semantics/repos.d.ts +17 -0
- package/dist/src/semantics/repos.js +390 -0
- package/dist/src/semantics/rulesets.d.ts +2 -0
- package/dist/src/semantics/rulesets.js +111 -0
- package/dist/src/semantics/search.d.ts +2 -0
- package/dist/src/semantics/search.js +84 -0
- package/dist/src/semantics/security.d.ts +2 -0
- package/dist/src/semantics/security.js +177 -0
- package/dist/src/semantics/shared.d.ts +61 -0
- package/dist/src/semantics/shared.js +151 -0
- package/dist/src/semantics/users.d.ts +2 -0
- package/dist/src/semantics/users.js +255 -0
- package/dist/src/semantics/webhooks.d.ts +2 -0
- package/dist/src/semantics/webhooks.js +107 -0
- package/dist/test-fixtures/github-a11y-reference.pr-list.SOURCE.md +56 -0
- package/dist/test-fixtures/github-a11y-reference.pr-list.json +1447 -0
- package/dist/test-fixtures/github-comment-schema.SOURCE.md +11 -0
- package/dist/test-fixtures/github-comment-schema.json +702 -0
- package/dist/test-fixtures/github-known-deviations.json +94 -0
- package/dist/test-fixtures/github-openapi-operations.SOURCE.md +76 -0
- package/dist/test-fixtures/github-openapi-operations.json +362 -0
- package/dist/test-fixtures/github-pull-schema.SOURCE.md +37 -0
- package/dist/test-fixtures/github-pull-schema.json +3601 -0
- package/dist/test-fixtures/github-review-schema.SOURCE.md +12 -0
- package/dist/test-fixtures/github-review-schema.json +236 -0
- package/package.json +20 -11
- package/src/cli.ts +7 -6
- package/src/generated/graphql-sdl.gen.json +1 -0
- package/src/generated/graphql.gen.json +1 -0
- package/src/generated/surface.gen.json +1 -0
- package/src/generated/ui.gen.json +1 -0
- package/src/github-a11y-snapshot.uitest.ts +5 -5
- package/src/github-budget.ts +4 -4
- package/src/github-capabilities.ts +1993 -556
- package/src/github-conformance.ts +12 -7
- package/src/github-connector.ts +108 -96
- package/src/github-events.ts +223 -95
- package/src/github-git-http.ts +47 -83
- package/src/github-git-plane.ts +247 -385
- package/src/github-journey.uitest.ts +28 -44
- package/src/github-mirror-state.ts +23 -61
- package/src/github-mirror-ui.ts +16 -10
- package/src/github-server.ts +158 -60
- package/src/github-shared.ts +1 -1
- package/src/github-twin.ts +1525 -4494
- package/src/github-ui-conformance.ts +7 -8
- package/src/github-ui-structure.ts +19 -18
- package/src/graphql-wire.ts +115 -0
- package/src/index.ts +47 -7
- package/src/manifest.ts +605 -0
- package/src/npm-registry.ts +43 -0
- package/src/screens/app-installation.tsx +217 -0
- package/src/screens/app-manifest.tsx +97 -0
- package/src/screens/oauth.tsx +258 -0
- package/src/screens/session.tsx +186 -0
- package/src/semantics/actions.ts +395 -0
- package/src/semantics/activity.ts +171 -0
- package/src/semantics/apps.ts +133 -0
- package/src/semantics/branches.ts +133 -0
- package/src/semantics/checks.ts +163 -0
- package/src/semantics/code-scanning-upload.ts +92 -0
- package/src/semantics/codespaces.ts +58 -0
- package/src/semantics/commits.ts +109 -0
- package/src/semantics/contents.ts +112 -0
- package/src/semantics/deployments.ts +116 -0
- package/src/semantics/gists.ts +85 -0
- package/src/semantics/git.ts +226 -0
- package/src/semantics/graphql.ts +505 -0
- package/src/semantics/index.ts +68 -0
- package/src/semantics/issues.ts +434 -0
- package/src/semantics/keys.ts +66 -0
- package/src/semantics/labels.ts +91 -0
- package/src/semantics/meta.ts +139 -0
- package/src/semantics/notifications.ts +58 -0
- package/src/semantics/orgs.ts +383 -0
- package/src/semantics/packages.ts +59 -0
- package/src/semantics/pages.ts +154 -0
- package/src/semantics/projects.ts +246 -0
- package/src/semantics/pulls.ts +421 -0
- package/src/semantics/push-reactions.ts +471 -0
- package/src/semantics/releases.ts +213 -0
- package/src/semantics/repo-invitations.ts +112 -0
- package/src/semantics/repos.ts +373 -0
- package/src/semantics/rulesets.ts +107 -0
- package/src/semantics/search.ts +83 -0
- package/src/semantics/security.ts +153 -0
- package/src/semantics/shared.ts +181 -0
- package/src/semantics/users.ts +252 -0
- package/src/semantics/webhooks.ts +101 -0
- package/test-fixtures/github-known-deviations.json +7 -8
- package/test-fixtures/github-openapi-operations.json +46 -227
- 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/
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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/
|
|
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:
|
|
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-
|
|
132
|
-
label: 'each PR row shows
|
|
133
|
-
present: count(dom.prList, 'class="state-
|
|
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
|
|
143
|
-
present: dom.conversation.includes('class="conversation
|
|
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
|
|
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: '
|
|
157
|
-
label: 'the
|
|
158
|
-
present: dom.
|
|
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
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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/
|
|
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 (
|
|
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,24 @@ export const pack: TwinPack = {
|
|
|
85
106
|
'APPSMITHOAUTH2GITHUB',
|
|
86
107
|
],
|
|
87
108
|
},
|
|
88
|
-
|
|
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
|
+
{ host: 'github.com', pathPattern: '^/apps/[^/]+/installations/new$' },
|
|
122
|
+
{ host: 'github.com', pathPattern: '^/(organizations/[^/]+/)?settings/apps/new$' },
|
|
123
|
+
{ host: 'npm.pkg.github.com', pathPattern: '^/@' },
|
|
124
|
+
],
|
|
89
125
|
};
|
|
126
|
+
|
|
127
|
+
// PROTOCOL 2: the pack registers itself on load — its rate budget and its state system (the head
|
|
128
|
+
// resolves `stateSystemFor(vendor)` in the process that serves the twin).
|
|
129
|
+
registerPack(pack);
|