@volter/twin-github 0.1.1 → 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
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type UiSurface, type UiCompletenessReport } from '@volter/world-tooling';
|
|
2
|
+
/** What real GitHub shows on the PR list, PR conversation, and issues list — classified vs the twin. */
|
|
3
|
+
export declare const GITHUB_UI_INVENTORY: UiSurface[];
|
|
4
|
+
export declare function githubUiConformance(): UiCompletenessReport;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// GitHub UI conformance (RUNG-5) — the declared UI surface inventory for a GitHub
|
|
2
|
+
// pull-request / issue view, classified against THIS twin, and the read-only
|
|
3
|
+
// completeness check over it.
|
|
4
|
+
//
|
|
5
|
+
// Honesty: `status` is the current truth, kept honest by github-ui-conformance.test.ts,
|
|
6
|
+
// which cross-checks every 'rendered' claim against the actual mirror (the bundled
|
|
7
|
+
// client + the `mirror` store payload). `rendered` = the UI mirror shows it today;
|
|
8
|
+
// `modeled` = the twin has the data but the mirror doesn't render it (a UI gap to
|
|
9
|
+
// close); `unmodeled` = real GitHub shows it but the EVIDENCE twin doesn't model it
|
|
10
|
+
// (a twin-model gap). The github world is an evidence mirror — OBSERVED PRs carry
|
|
11
|
+
// metadata + review/comment COUNTS but never titles, bodies, authors, diffs, or
|
|
12
|
+
// per-commit/per-file content. LOCAL simulator/fork writes, however, DO carry content,
|
|
13
|
+
// including per-file diffs, per-commit detail, and additions/deletions line counts —
|
|
14
|
+
// so those are MODELED features and the mirror renders them (empty for observed-only
|
|
15
|
+
// PRs, preserving honesty). What stays 'unmodeled' is what the twin models for NEITHER
|
|
16
|
+
// observed nor local writes: PR author identity and the head branch NAME.
|
|
17
|
+
import { checkUiCompleteness } from '@volter/world-tooling';
|
|
18
|
+
/** What real GitHub shows on the PR list, PR conversation, and issues list — classified vs the twin. */
|
|
19
|
+
export const GITHUB_UI_INVENTORY = [
|
|
20
|
+
// --- rendered: the mirror truly shows these today (proven in the test) ---
|
|
21
|
+
{ key: 'prNumber', label: 'PR number (#123)', status: 'rendered' },
|
|
22
|
+
{ key: 'title', label: 'PR title', status: 'rendered' },
|
|
23
|
+
{ key: 'body', label: 'PR description / body', status: 'rendered' },
|
|
24
|
+
{ key: 'stateBadge', label: 'State / merge badge (open/closed/merged)', status: 'rendered' },
|
|
25
|
+
{ key: 'labels', label: 'Labels', status: 'rendered' },
|
|
26
|
+
{ key: 'assignees', label: 'Assignees', status: 'rendered' },
|
|
27
|
+
{ key: 'reviewers', label: 'Reviewers (state + login)', status: 'rendered' },
|
|
28
|
+
{ key: 'reviewContent', label: 'Review submissions (body + state)', status: 'rendered' },
|
|
29
|
+
{ key: 'comments', label: 'Issue comments / conversation', status: 'rendered' },
|
|
30
|
+
{ key: 'reviewComments', label: 'Review (diff-thread) comments', status: 'rendered' },
|
|
31
|
+
{ key: 'changedFilesCount', label: 'Changed-files count', status: 'rendered' },
|
|
32
|
+
{ key: 'commitsCount', label: 'Commits count', status: 'rendered' },
|
|
33
|
+
{ key: 'reviewCount', label: 'Review count', status: 'rendered' },
|
|
34
|
+
{ key: 'commentsCount', label: 'Comment count', status: 'rendered' },
|
|
35
|
+
{ key: 'baseBranch', label: 'Base branch ref', status: 'rendered' },
|
|
36
|
+
{ key: 'headSha', label: 'Head commit sha', status: 'rendered' },
|
|
37
|
+
{ key: 'mergeCommit', label: 'Merge commit sha', status: 'rendered' },
|
|
38
|
+
{ key: 'mergedAt', label: 'Merged timestamp', status: 'rendered' },
|
|
39
|
+
{ key: 'prUrl', label: 'Link to PR on GitHub', status: 'rendered' },
|
|
40
|
+
{ key: 'issuesList', label: 'Issues list (first-class issues view)', status: 'rendered' },
|
|
41
|
+
{ key: 'repoSelector', label: 'Per-repo selector (repo grouping)', status: 'rendered' },
|
|
42
|
+
// reviewCommentsCount counts into the pull request's Conversation tab, with its comments and reviews.
|
|
43
|
+
{ key: 'reviewCommentsCount', label: 'Review-comment (diff-thread) count', status: 'rendered' },
|
|
44
|
+
// CI rollup (commit statuses + check-runs) over the PR head_sha — the twin now models
|
|
45
|
+
// statuses[]/checkRuns[]; the mirror renders a ✓/✗/● indicator, the merge box's checks and a Checks tab.
|
|
46
|
+
{ key: 'checksStatus', label: 'CI / status checks', status: 'rendered' },
|
|
47
|
+
// milestone: the twin models milestone NUMBER + state.milestones; the mirror renders
|
|
48
|
+
// the milestone title as a badge on PR/issue rows + a Milestone side-section.
|
|
49
|
+
{ key: 'milestone', label: 'Milestone', status: 'rendered' },
|
|
50
|
+
// linked PRs/issues: the twin models issue.linked[]; the mirror renders typed
|
|
51
|
+
// cross-reference links on the issue rows.
|
|
52
|
+
{ key: 'linkedIssues', label: 'Linked / source issue', status: 'rendered' },
|
|
53
|
+
// requested reviewers (distinct from submitted reviews): the twin models
|
|
54
|
+
// requested_reviewers[]; the mirror lists them, awaiting review, in the Reviewers sidebar section.
|
|
55
|
+
{ key: 'requestedReviewers', label: 'Requested reviewers', status: 'rendered' },
|
|
56
|
+
// per-file diffs: the twin models pr.files (GithubPullFile) on LOCAL writes; the mirror
|
|
57
|
+
// renders the Files changed tab (each file's path, +/- counts and unified diff).
|
|
58
|
+
// Empty for observed-only PRs (no per-file evidence) — honesty preserved.
|
|
59
|
+
{ key: 'filesChangedContent', label: 'Per-file diff list (filenames + status + counts)', status: 'rendered' },
|
|
60
|
+
// additions/deletions: the twin rolls these up from the carried file list on LOCAL writes;
|
|
61
|
+
// the mirror renders them as the pull request's diffstat. Undefined for observed-only PRs.
|
|
62
|
+
{ key: 'diffStats', label: 'Additions / deletions line counts', status: 'rendered' },
|
|
63
|
+
// per-commit detail: the twin models pr.commits_list (GithubPullCommit) on LOCAL writes;
|
|
64
|
+
// the mirror renders them in the timeline and the Commits tab (sha + message + author + date).
|
|
65
|
+
// Empty for observed-only PRs (only a commit COUNT is observed) — honesty preserved.
|
|
66
|
+
{ key: 'commitsContent', label: 'Per-commit list (sha + message + author)', status: 'rendered' },
|
|
67
|
+
// --- Actions (workflows + runs + jobs) — a LOCAL construct (running real CI is the only
|
|
68
|
+
// Actions non-goal; the run/job OBJECTS are modeled + rendered in the Actions view) ---
|
|
69
|
+
// the Actions tab lists workflow runs; each run row carries a status/conclusion badge.
|
|
70
|
+
{ key: 'actionsRunsList', label: 'Workflow runs list (Actions view)', status: 'rendered' },
|
|
71
|
+
// the run badge reflects status (queued/in_progress/completed) + conclusion (success/
|
|
72
|
+
// failure/cancelled), rendered as a glyph + label on every run row + the detail header.
|
|
73
|
+
{ key: 'runStatusBadge', label: 'Run status / conclusion badge', status: 'rendered' },
|
|
74
|
+
// selecting a run renders a detail pane with its jobs + each job's steps.
|
|
75
|
+
{ key: 'runJobs', label: 'Run detail — jobs + steps', status: 'rendered' },
|
|
76
|
+
// --- Releases (workflows of the repo's release channel) — a LOCAL construct (no observed
|
|
77
|
+
// release evidence; release ASSET binary BYTES are the only Releases non-goal) ---
|
|
78
|
+
// the Releases tab lists releases; each row carries tag + name + draft/prerelease badge.
|
|
79
|
+
{ key: 'releasesList', label: 'Releases list (Releases view)', status: 'rendered' },
|
|
80
|
+
// the channel badge reflects draft / prerelease / published, rendered on every row + header.
|
|
81
|
+
{ key: 'releaseChannelBadge', label: 'Release channel badge (draft/prerelease/release)', status: 'rendered' },
|
|
82
|
+
// selecting a release renders a detail pane with its notes + ASSET catalog (metadata only).
|
|
83
|
+
{ key: 'releaseAssets', label: 'Release detail — asset catalog (name/type/size)', status: 'rendered' },
|
|
84
|
+
// --- Discussions (a LOCAL construct; modeled over REST — a declared deviation from
|
|
85
|
+
// GitHub's GraphQL Discussions API. No observed discussion evidence) ---
|
|
86
|
+
// the Discussions tab lists discussions; each row carries title + category + answer badge.
|
|
87
|
+
{ key: 'discussionsList', label: 'Discussions list (Discussions view)', status: 'rendered' },
|
|
88
|
+
// the category chip reflects the discussion's category (Q&A categories are answerable).
|
|
89
|
+
{ key: 'discussionCategory', label: 'Discussion category chip (General/Q&A/…)', status: 'rendered' },
|
|
90
|
+
// an answerable (Q&A) discussion shows an answered/unanswered badge.
|
|
91
|
+
{ key: 'discussionAnswerStatus', label: 'Discussion answered/unanswered status (Q&A)', status: 'rendered' },
|
|
92
|
+
// selecting a discussion renders a detail pane with the opening post + the comment/reply
|
|
93
|
+
// thread, highlighting the accepted answer.
|
|
94
|
+
{ key: 'discussionThread', label: 'Discussion detail — comment/reply thread + accepted answer', status: 'rendered' },
|
|
95
|
+
// --- unmodeled: real GitHub shows it, the evidence twin models it for neither
|
|
96
|
+
// observed nor local writes (a genuine twin-model gap) ---
|
|
97
|
+
// The twin never carries author identity on a PR (observed evidence has only repo/number/
|
|
98
|
+
// counts; local writes don't capture the acting login into the PR object either).
|
|
99
|
+
{ key: 'author', label: 'PR author (login + avatar)', status: 'unmodeled' },
|
|
100
|
+
// The twin tracks base_ref + head_sha but not the head branch NAME (only the head sha).
|
|
101
|
+
{ key: 'headBranch', label: 'Head branch ref name', status: 'unmodeled' },
|
|
102
|
+
];
|
|
103
|
+
export function githubUiConformance() {
|
|
104
|
+
return checkUiCompleteness('github', GITHUB_UI_INVENTORY);
|
|
105
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type UiStructureCheck, type UiStructureReport } from '@volter/world-tooling';
|
|
2
|
+
/** Render the exported presentational components to static HTML, once, deterministically. */
|
|
3
|
+
export declare function renderGithubMirrorMarkup(): {
|
|
4
|
+
prList: string;
|
|
5
|
+
conversation: string;
|
|
6
|
+
aside: string;
|
|
7
|
+
files: string;
|
|
8
|
+
issues: string;
|
|
9
|
+
ciStatus: string;
|
|
10
|
+
actions: string;
|
|
11
|
+
releases: string;
|
|
12
|
+
discussions: string;
|
|
13
|
+
deployments: string;
|
|
14
|
+
};
|
|
15
|
+
/** The structural checklist: each `present` is derived from the rendered markup above. */
|
|
16
|
+
export declare function githubUiStructureChecks(): UiStructureCheck[];
|
|
17
|
+
/** RUNG-5 structural DOM report for the GitHub mirror. Read-only, deterministic. */
|
|
18
|
+
export declare function githubUiStructure(): UiStructureReport;
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
// RUNG-5 STRUCTURAL DOM CHECKLIST for the GitHub mirror — the PROXIMITY half of the
|
|
2
|
+
// UI rung. Completeness asks "does the mirror show the data?"; this asks "does the
|
|
3
|
+
// mirror's rendered DOM look structurally like the real GitHub UI?" — the PR table
|
|
4
|
+
// emits a row per PR, each row carries its state icon, the conversation pane has the
|
|
5
|
+
// reviews/comments timeline, the Aside carries the reviewers/labels/assignees/milestone
|
|
6
|
+
// sections, a CI status element renders, the issues view emits a row per issue.
|
|
7
|
+
//
|
|
8
|
+
// Each check's `present` is computed from the ACTUAL markup produced by
|
|
9
|
+
// renderToStaticMarkup of the EXPORTED mirror components with seeded props — so a check
|
|
10
|
+
// cannot pass unless the component truly emits that landmark. Not the bundle, not the
|
|
11
|
+
// inventory: the real React render.
|
|
12
|
+
import { renderToStaticMarkup } from 'react-dom/server';
|
|
13
|
+
import { createElement } from 'react';
|
|
14
|
+
import { checkUiStructure } from '@volter/world-tooling';
|
|
15
|
+
import { PrList, Conversation, Aside, FilesDiff, IssuesTable, CiStatus, ActionsView, ReleasesView, DiscussionsView, DeploymentsView, } from "../client/github-mirror.js";
|
|
16
|
+
// ── Seeded props: one rich PR (reviews/comments/labels/assignees/CI/milestone/files)
|
|
17
|
+
// plus a second PR so the list has more than one row, and one issue. ───────────────
|
|
18
|
+
const PR = {
|
|
19
|
+
repo: 'o/r', number: 1, title: 'Add feature', body: 'why this PR', state: 'open',
|
|
20
|
+
author: 'octocat', baseBranch: 'main', headBranch: 'feat', headSha: 'abcdef1234567',
|
|
21
|
+
createdAt: '2026-04-01T00:00:00Z', updatedAt: '2026-04-02T00:00:00Z',
|
|
22
|
+
additions: 42, deletions: 7, changedFiles: 2, commitsCount: 3,
|
|
23
|
+
commentsCount: 1, reviewCount: 1, reviewCommentsCount: 1,
|
|
24
|
+
labels: [{ name: 'bug' }, { name: 'enhancement' }],
|
|
25
|
+
assignees: ['alice'], requestedReviewers: ['carol'], milestone: 'v1.0',
|
|
26
|
+
ciStatus: 'success',
|
|
27
|
+
};
|
|
28
|
+
const PR_TWO = {
|
|
29
|
+
repo: 'o/r', number: 2, title: 'Fix bug', state: 'open', author: 'hubot',
|
|
30
|
+
createdAt: '2026-03-01T00:00:00Z', updatedAt: '2026-03-02T00:00:00Z', ciStatus: 'failure',
|
|
31
|
+
};
|
|
32
|
+
const ISSUE = {
|
|
33
|
+
repo: 'o/web', number: 1, title: 'An issue', state: 'open',
|
|
34
|
+
createdAt: '2026-04-04T00:00:00Z', updatedAt: '2026-04-04T00:00:00Z',
|
|
35
|
+
labels: [{ name: 'triage' }], assignees: ['bob'], milestone: 'v1.0',
|
|
36
|
+
};
|
|
37
|
+
const ISSUE_TWO = {
|
|
38
|
+
repo: 'o/web', number: 2, title: 'Another issue', state: 'closed',
|
|
39
|
+
createdAt: '2026-04-05T00:00:00Z', updatedAt: '2026-04-05T00:00:00Z',
|
|
40
|
+
};
|
|
41
|
+
const REVIEWS = [{ repo: 'o/r', number: 1, reviewer: 'carol', state: 'approved', body: 'looks good', submittedAt: '2026-04-02T01:00:00Z' }];
|
|
42
|
+
const COMMITS = [{ repo: 'o/r', number: 1, sha: 'abcdef1234567', message: 'do the thing', authorLogin: 'octocat', date: '2026-04-01T12:00:00Z' }];
|
|
43
|
+
const COMMENTS = [{ repo: 'o/r', number: 1, author: 'dave', body: 'a note', createdAt: '2026-04-02T02:00:00Z' }];
|
|
44
|
+
const REVIEW_COMMENTS = [{ repo: 'o/r', number: 1, author: 'erin', body: 'inline diff note', path: 'src/x.ts', createdAt: '2026-04-02T03:00:00Z' }];
|
|
45
|
+
const FILES = [{ 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' }];
|
|
46
|
+
// Actions: two runs (one completed/success, one cancelled) so the runs list has >1 row, plus
|
|
47
|
+
// a job (with steps) for the first run so the run-detail jobs section renders.
|
|
48
|
+
const RUNS = [
|
|
49
|
+
{ repo: 'o/r', id: 101, workflowId: 1, workflowName: 'CI', runNumber: 1, event: 'workflow_dispatch', status: 'completed', conclusion: 'success', headSha: 'abcdef1234567', headBranch: 'main', jobCount: 1, url: 'https://github.com/o/r/actions/runs/101' },
|
|
50
|
+
{ repo: 'o/r', id: 102, workflowId: 1, workflowName: 'CI', runNumber: 2, event: 'workflow_dispatch', status: 'completed', conclusion: 'cancelled', headSha: 'abcdef7654321', headBranch: 'dev', jobCount: 0 },
|
|
51
|
+
];
|
|
52
|
+
const JOBS = [{ repo: 'o/r', id: 201, runId: 101, name: 'build', status: 'completed', conclusion: 'success', steps: [{ name: 'set up job', status: 'completed', conclusion: 'success', number: 1 }] }];
|
|
53
|
+
// Releases: two releases (one published with an asset, one draft) so the list has >1 row and
|
|
54
|
+
// the detail pane renders the asset catalog (metadata only — bytes are the Non-goal).
|
|
55
|
+
const RELEASES = [
|
|
56
|
+
{ repo: 'o/r', id: 1, tagName: 'v1.0.0', name: 'First release', body: 'the notes', draft: false, prerelease: false, publishedAt: '2026-04-10T00:00:00Z', targetCommitish: 'main', url: 'https://github.com/o/r/releases/tag/v1.0.0', assets: [{ id: 11, name: 'app.zip', contentType: 'application/zip', size: 2048, downloadCount: 3 }] },
|
|
57
|
+
{ repo: 'o/r', id: 2, tagName: 'v1.1.0-rc1', name: 'RC', draft: true, prerelease: true, createdAt: '2026-04-11T00:00:00Z', targetCommitish: 'main', assets: [] },
|
|
58
|
+
];
|
|
59
|
+
// Discussions: two discussions (one answerable Q&A with a chosen answer + a reply, one
|
|
60
|
+
// General) so the list has >1 row and the detail pane renders the comment thread + the
|
|
61
|
+
// accepted-answer landmark.
|
|
62
|
+
const DISCUSSIONS = [
|
|
63
|
+
{
|
|
64
|
+
repo: 'o/r', number: 1, title: 'How do I configure X?', body: 'I am stuck on X.', state: 'open',
|
|
65
|
+
categorySlug: 'q-a', categoryName: 'Q&A', categoryEmoji: '🙏', isAnswerable: true,
|
|
66
|
+
answerCommentId: 101, isAnswered: true, commentsCount: 2, updatedAt: '2026-04-12T00:00:00Z',
|
|
67
|
+
url: 'https://github.com/o/r/discussions/1',
|
|
68
|
+
comments: [
|
|
69
|
+
{ id: 101, parentId: null, body: 'Do it this way.', isAnswer: true, createdAt: '2026-04-12T01:00:00Z' },
|
|
70
|
+
{ id: 102, parentId: 101, body: 'Thanks, that worked!', isAnswer: false, createdAt: '2026-04-12T02:00:00Z' },
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
repo: 'o/r', number: 2, title: 'Welcome!', body: 'Say hi here.', state: 'open',
|
|
75
|
+
categorySlug: 'general', categoryName: 'General', categoryEmoji: '💬', isAnswerable: false,
|
|
76
|
+
answerCommentId: null, isAnswered: false, commentsCount: 0, updatedAt: '2026-04-13T00:00:00Z', comments: [],
|
|
77
|
+
},
|
|
78
|
+
];
|
|
79
|
+
// Deployments: two deployments (one success in production, one pending in staging) so the
|
|
80
|
+
// list has >1 row, plus two environments (one protected with a wait timer + reviewers, one
|
|
81
|
+
// unprotected) so the environments list renders with a protection summary.
|
|
82
|
+
const DEPLOYMENTS = [
|
|
83
|
+
{ repo: 'o/r', id: 1, ref: 'main', sha: 'abcdef1234567', environment: 'production', task: 'deploy', description: 'release v1', production: true, state: 'success', environmentUrl: 'https://app.example.com', createdAt: '2026-04-10T00:00:00Z', statuses: [{ id: 2, state: 'success' }, { id: 1, state: 'in_progress' }] },
|
|
84
|
+
{ repo: 'o/r', id: 2, ref: 'dev', sha: 'fedcba7654321', environment: 'staging', task: 'deploy', state: 'pending', createdAt: '2026-04-11T00:00:00Z', statuses: [] },
|
|
85
|
+
];
|
|
86
|
+
const ENVIRONMENTS = [
|
|
87
|
+
{ repo: 'o/r', name: 'production', waitTimer: 30, reviewers: ['alice', 'bob'], protected: true },
|
|
88
|
+
{ repo: 'o/r', name: 'staging', reviewers: [], protected: false },
|
|
89
|
+
];
|
|
90
|
+
/** Count non-overlapping occurrences of a substring in `html`. */
|
|
91
|
+
function count(html, marker) {
|
|
92
|
+
if (!marker)
|
|
93
|
+
return 0;
|
|
94
|
+
let n = 0;
|
|
95
|
+
let i = html.indexOf(marker);
|
|
96
|
+
while (i !== -1) {
|
|
97
|
+
n += 1;
|
|
98
|
+
i = html.indexOf(marker, i + marker.length);
|
|
99
|
+
}
|
|
100
|
+
return n;
|
|
101
|
+
}
|
|
102
|
+
/** Render the exported presentational components to static HTML, once, deterministically. */
|
|
103
|
+
export function renderGithubMirrorMarkup() {
|
|
104
|
+
return {
|
|
105
|
+
prList: renderToStaticMarkup(createElement(PrList, { prs: [PR, PR_TWO], reviews: REVIEWS, selectedKey: 'o/r#1' })),
|
|
106
|
+
conversation: renderToStaticMarkup(createElement(Conversation, { pr: PR, reviews: REVIEWS, commits: COMMITS, comments: COMMENTS, reviewComments: REVIEW_COMMENTS, files: FILES })),
|
|
107
|
+
aside: renderToStaticMarkup(createElement(Aside, { pr: PR, reviews: REVIEWS, commits: COMMITS, files: FILES })),
|
|
108
|
+
files: renderToStaticMarkup(createElement(FilesDiff, { files: FILES })),
|
|
109
|
+
issues: renderToStaticMarkup(createElement(IssuesTable, { issues: [ISSUE, ISSUE_TWO] })),
|
|
110
|
+
ciStatus: renderToStaticMarkup(createElement(CiStatus, { status: 'success' })),
|
|
111
|
+
actions: renderToStaticMarkup(createElement(ActionsView, { runs: RUNS, jobs: JOBS, selectedRunId: 101 })),
|
|
112
|
+
releases: renderToStaticMarkup(createElement(ReleasesView, { releases: RELEASES, selectedReleaseId: 1 })),
|
|
113
|
+
discussions: renderToStaticMarkup(createElement(DiscussionsView, { discussions: DISCUSSIONS, selectedKey: 'o/r#1' })),
|
|
114
|
+
deployments: renderToStaticMarkup(createElement(DeploymentsView, { deployments: DEPLOYMENTS, environments: ENVIRONMENTS })),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/** The structural checklist: each `present` is derived from the rendered markup above. */
|
|
118
|
+
export function githubUiStructureChecks() {
|
|
119
|
+
const dom = renderGithubMirrorMarkup();
|
|
120
|
+
return [
|
|
121
|
+
{
|
|
122
|
+
key: 'pr-list-row-per-pr',
|
|
123
|
+
label: 'the PR list emits a row (.pr-row) per pull request',
|
|
124
|
+
present: count(dom.prList, 'class="pr-row"') === 2,
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
key: 'pr-row-state-icon',
|
|
128
|
+
label: 'each PR row shows its open/merged/closed state icon (.pr-state-icon)',
|
|
129
|
+
present: count(dom.prList, 'class="pr-state-icon') === 2,
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
key: 'pr-row-ci-status',
|
|
133
|
+
label: 'a PR row renders a CI status indicator (.ci-status)',
|
|
134
|
+
present: dom.prList.includes('class="ci-status'),
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
key: 'conversation-detail-pane',
|
|
138
|
+
label: 'the conversation pane renders (section.conversation with the merge box)',
|
|
139
|
+
present: dom.conversation.includes('class="conversation') && dom.conversation.includes('class="gh-mergebox"'),
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
key: 'conversation-comments-timeline',
|
|
143
|
+
label: 'the conversation pane has a comments timeline (.timeline-item with a .comment)',
|
|
144
|
+
present: dom.conversation.includes('class="timeline-item') && dom.conversation.includes('class="comment'),
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
key: 'conversation-reviews-section',
|
|
148
|
+
label: 'the conversation pane has a reviews section (.review-card)',
|
|
149
|
+
present: dom.conversation.includes('class="review-card'),
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
key: 'files-changed-diff',
|
|
153
|
+
label: 'the Files changed tab renders each file with its diff rows (.gh-file with .gh-diff-add and .gh-diff-del)',
|
|
154
|
+
present: dom.files.includes('class="diff-file gh-file"') && dom.files.includes('class="gh-diff-add"') && dom.files.includes('class="gh-diff-del"'),
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
key: 'aside-sidebar',
|
|
158
|
+
label: 'the detail Aside renders as a sidebar landmark (aside.sidebar)',
|
|
159
|
+
present: dom.aside.includes('<aside class="sidebar'),
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
key: 'aside-reviewers-section',
|
|
163
|
+
label: 'the Aside has a Reviewers section',
|
|
164
|
+
present: dom.aside.includes('<h3>Reviewers<'),
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
key: 'aside-labels-assignees-milestone',
|
|
168
|
+
label: 'the Aside has Labels, Assignees and Milestone sections',
|
|
169
|
+
present: dom.aside.includes('<h3>Labels<') && dom.aside.includes('<h3>Assignees<') && dom.aside.includes('<h3>Milestone<'),
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
key: 'ci-status-element',
|
|
173
|
+
label: 'the CI status component renders a labelled status element (.ci-status.success)',
|
|
174
|
+
present: dom.ciStatus.includes('class="ci-status success"') && dom.ciStatus.includes('title="checks: success"'),
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
key: 'issues-row-per-issue',
|
|
178
|
+
label: 'the issues view emits a row (.issue-row) per issue',
|
|
179
|
+
present: count(dom.issues, 'issue-row') === 2,
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
key: 'actions-run-row-per-run',
|
|
183
|
+
label: 'the Actions view emits a run row (.run-row) per workflow run',
|
|
184
|
+
present: count(dom.actions, 'class="run-row') === 2,
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
key: 'actions-run-status-badge',
|
|
188
|
+
label: 'each run row shows a run status/conclusion badge (.run-badge)',
|
|
189
|
+
present: count(dom.actions, 'class="run-badge') >= 2,
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
key: 'actions-run-detail-jobs',
|
|
193
|
+
label: 'the run detail pane renders a jobs section (.job-list with a .job-row + .step-row)',
|
|
194
|
+
present: dom.actions.includes('class="job-list"') && dom.actions.includes('class="job-row"') && dom.actions.includes('class="step-row"'),
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
key: 'releases-row-per-release',
|
|
198
|
+
label: 'the Releases view emits a release row (.release-row) per release',
|
|
199
|
+
present: count(dom.releases, 'class="release-row') === 2,
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
key: 'releases-channel-badge',
|
|
203
|
+
label: 'each release row shows a draft/prerelease/release channel badge (.release-badge)',
|
|
204
|
+
present: count(dom.releases, 'class="release-badge') >= 2,
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
key: 'releases-detail-assets',
|
|
208
|
+
label: 'the release detail pane renders an asset catalog (.asset-list with an .asset-row)',
|
|
209
|
+
present: dom.releases.includes('class="asset-list"') && dom.releases.includes('class="asset-row"'),
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
key: 'discussions-row-per-discussion',
|
|
213
|
+
label: 'the Discussions view emits a discussion row (.discussion-row) per discussion',
|
|
214
|
+
present: count(dom.discussions, 'class="discussion-row') === 2,
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
key: 'discussions-category-badge',
|
|
218
|
+
label: 'each discussion row shows a category chip (.discussion-category)',
|
|
219
|
+
present: count(dom.discussions, 'class="discussion-category') >= 2,
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
key: 'discussions-answer-badge',
|
|
223
|
+
label: 'an answerable discussion shows an answered/unanswered badge (.answer-badge)',
|
|
224
|
+
present: dom.discussions.includes('class="answer-badge'),
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
key: 'discussions-detail-thread',
|
|
228
|
+
label: 'the discussion detail pane renders a comment thread (.comment-thread with a .thread-comment) + the accepted answer (.accepted-answer)',
|
|
229
|
+
present: dom.discussions.includes('class="comment-thread"') && dom.discussions.includes('class="thread-comment') && dom.discussions.includes('class="comment accepted-answer"'),
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
key: 'deployments-row-per-deployment',
|
|
233
|
+
label: 'the Deployments view emits a deployment row (.deployment-row) per deployment',
|
|
234
|
+
present: count(dom.deployments, 'class="deployment-row') === 2,
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
key: 'deployments-state-badge',
|
|
238
|
+
label: 'each deployment row shows a state badge (.deployment-state)',
|
|
239
|
+
present: count(dom.deployments, 'class="deployment-state') >= 2,
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
key: 'deployments-environments-list',
|
|
243
|
+
label: 'the Deployments view renders an environments list (.environments-list with an .environment-row + a protection summary)',
|
|
244
|
+
present: dom.deployments.includes('class="environments-list"') && dom.deployments.includes('class="environment-row"') && dom.deployments.includes('class="environment-protected'),
|
|
245
|
+
},
|
|
246
|
+
];
|
|
247
|
+
}
|
|
248
|
+
/** RUNG-5 structural DOM report for the GitHub mirror. Read-only, deterministic. */
|
|
249
|
+
export function githubUiStructure() {
|
|
250
|
+
return checkUiStructure('github', githubUiStructureChecks());
|
|
251
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type GraphqlContext = Record<string, unknown> & {
|
|
2
|
+
request: Request;
|
|
3
|
+
};
|
|
4
|
+
export type FieldResolver = (source: Record<string, unknown>, args: Record<string, unknown>, context: GraphqlContext) => unknown;
|
|
5
|
+
export type GraphqlWireOptions = {
|
|
6
|
+
/** the vendor's schema, loaded on the first GraphQL request */
|
|
7
|
+
loadSdl: () => Promise<string>;
|
|
8
|
+
/** resolvers by `Type.field` (`Query.repository`, `PullRequest.state`) */
|
|
9
|
+
resolvers: Record<string, FieldResolver>;
|
|
10
|
+
/** per type, a GraphQL field name → the stored field that holds it (`nameWithOwner` → `full_name`) */
|
|
11
|
+
aliases?: Record<string, Record<string, string>>;
|
|
12
|
+
/** per type, the stored fields the default resolver may read as they are */
|
|
13
|
+
stored?: Record<string, string[]>;
|
|
14
|
+
/** what every resolver of one request receives beside the request (a pack's handler interface) */
|
|
15
|
+
context: (request: Request) => Record<string, unknown> | Promise<Record<string, unknown>>;
|
|
16
|
+
/** the answer to a query that selects a field nothing models: the first such field and its type */
|
|
17
|
+
unmodeled: (field: {
|
|
18
|
+
type: string;
|
|
19
|
+
field: string;
|
|
20
|
+
}) => Response;
|
|
21
|
+
};
|
|
22
|
+
export declare function createGraphqlWire(options: GraphqlWireOptions): (request: Request) => Promise<Response>;
|
|
@@ -0,0 +1,89 @@
|
|
|
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 { execute, getNamedType, GraphQLError, isEnumType, isObjectType, parse, TypeInfo, validate, visit, visitWithTypeInfo, buildSchema, } from 'graphql';
|
|
13
|
+
const snake = (s) => s.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
|
|
14
|
+
function connection(items, args) {
|
|
15
|
+
const first = typeof args.first === 'number' ? args.first : undefined;
|
|
16
|
+
const last = typeof args.last === 'number' ? args.last : undefined;
|
|
17
|
+
const after = typeof args.after === 'string' ? Number(atob(args.after)) + 1 : 0;
|
|
18
|
+
let slice = items.slice(after);
|
|
19
|
+
if (first !== undefined)
|
|
20
|
+
slice = slice.slice(0, first);
|
|
21
|
+
if (last !== undefined)
|
|
22
|
+
slice = slice.slice(-last);
|
|
23
|
+
const start = after + (last !== undefined ? Math.max(0, items.slice(after).length - last) : 0);
|
|
24
|
+
const cursor = (i) => btoa(String(start + i));
|
|
25
|
+
return {
|
|
26
|
+
totalCount: items.length,
|
|
27
|
+
nodes: slice,
|
|
28
|
+
edges: slice.map((node, i) => ({ node, cursor: cursor(i) })),
|
|
29
|
+
pageInfo: { hasNextPage: start + slice.length < items.length, hasPreviousPage: start > 0, startCursor: slice.length ? cursor(0) : null, endCursor: slice.length ? cursor(slice.length - 1) : null },
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/** A query GitHub cannot parse: its syntax error, as GraphQL answers it. */
|
|
33
|
+
function syntaxError(error) {
|
|
34
|
+
return Response.json({ errors: [{ message: error instanceof GraphQLError ? error.message : String(error) }] });
|
|
35
|
+
}
|
|
36
|
+
export function createGraphqlWire(options) {
|
|
37
|
+
let schema;
|
|
38
|
+
const modeled = (type, field) => field === '__typename' ||
|
|
39
|
+
`${type}.${field}` in options.resolvers ||
|
|
40
|
+
!!options.aliases?.[type]?.[field] ||
|
|
41
|
+
!!options.stored?.[type]?.includes(field) ||
|
|
42
|
+
!!options.stored?.[type]?.includes(snake(field));
|
|
43
|
+
const fieldResolver = (source, args, context, info) => {
|
|
44
|
+
const own = options.resolvers[`${info.parentType.name}.${info.fieldName}`];
|
|
45
|
+
const obj = (source ?? {});
|
|
46
|
+
let value = own ? own(obj, args, context) : (obj[options.aliases?.[info.parentType.name]?.[info.fieldName] ?? info.fieldName] ?? obj[snake(info.fieldName)] ?? null);
|
|
47
|
+
const named = getNamedType(info.returnType);
|
|
48
|
+
if (Array.isArray(value) && isObjectType(named) && named.name.endsWith('Connection'))
|
|
49
|
+
value = connection(value, args);
|
|
50
|
+
if (typeof value === 'string' && isEnumType(named))
|
|
51
|
+
value = named.getValues().find((v) => v.name.toLowerCase() === value.toString().toLowerCase())?.name ?? value;
|
|
52
|
+
return value;
|
|
53
|
+
};
|
|
54
|
+
return async (request) => {
|
|
55
|
+
schema ??= buildSchema(await options.loadSdl(), { assumeValidSDL: true });
|
|
56
|
+
const payload = (await request.clone().json().catch(() => ({})));
|
|
57
|
+
let document;
|
|
58
|
+
try {
|
|
59
|
+
document = parse(payload.query ?? '');
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
return syntaxError(error);
|
|
63
|
+
}
|
|
64
|
+
const invalid = validate(schema, document);
|
|
65
|
+
if (invalid.length)
|
|
66
|
+
return Response.json({ errors: invalid.map((e) => ({ message: e.message, locations: e.locations })) });
|
|
67
|
+
// every selected field modeled, or the existing resolver answers
|
|
68
|
+
const typeInfo = new TypeInfo(schema);
|
|
69
|
+
let unmodeled;
|
|
70
|
+
visit(document, visitWithTypeInfo(typeInfo, {
|
|
71
|
+
Field(node) {
|
|
72
|
+
const parent = typeInfo.getParentType();
|
|
73
|
+
if (!unmodeled && parent && isObjectType(parent) && !parent.name.endsWith('Connection') && !parent.name.endsWith('Edge') && parent.name !== 'PageInfo' && !modeled(parent.name, node.name.value))
|
|
74
|
+
unmodeled = { type: parent.name, field: node.name.value };
|
|
75
|
+
},
|
|
76
|
+
}));
|
|
77
|
+
if (unmodeled)
|
|
78
|
+
return options.unmodeled(unmodeled);
|
|
79
|
+
const result = await execute({
|
|
80
|
+
schema,
|
|
81
|
+
document,
|
|
82
|
+
variableValues: payload.variables ?? {},
|
|
83
|
+
...(payload.operationName ? { operationName: payload.operationName } : {}),
|
|
84
|
+
contextValue: { ...(await options.context(request)), request },
|
|
85
|
+
fieldResolver,
|
|
86
|
+
});
|
|
87
|
+
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 })) } : {}) });
|
|
88
|
+
};
|
|
89
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { applyGithubWrite, githubState, handleGithubRequest, syncPrHeadsToBranchTip, toGithubComment, toGithubIssue, toGithubRest, toGithubReview, toGithubReviewComment } from './github-twin.js';
|
|
2
|
+
export { bareRepoDir, ensureBareRepo, hasBareRepo, listGitPlaneRefs, objectExistsInPlane, readGitBlobFromPlane, readGitCommitFromPlane, readGitTagFromPlane, readGitTreeFromPlane, updateGitRefInPlane, writeGitBlobToPlane, writeGitCommitToPlane, writeGitTagToPlane, writeGitTreeToPlane, } from './github-git-plane.js';
|
|
3
|
+
export { handleGithubGitSmartHttp, parseGitSmartHttpPath, reconcileGitPlaneRefs } from './github-git-http.js';
|
|
4
|
+
export type { GitSmartHttpResponse, GitSmartHttpRoute } from './github-git-http.js';
|
|
5
|
+
export type { GithubComment, GithubIssue, GithubPr, GithubResponse, GithubReview, GithubState, GithubWriteEvent, GithubWriteOutcome } from './github-twin.js';
|
|
6
|
+
export { createGithubTwinFetch, createGithubTwinServer } from './github-server.js';
|
|
7
|
+
export { emitGithubEvent } from './github-events.js';
|
|
8
|
+
export type { GithubWebhook, GithubWebhookDelivery } from './github-events.js';
|
|
9
|
+
export { buildGithubMirrorClient, createGithubMirrorServer, githubMirrorHtml } from './github-mirror-ui.js';
|
|
10
|
+
export { githubMirrorState } from './github-mirror-state.js';
|
|
11
|
+
export { GITHUB_MIRROR_STORE, GITHUB_MIRROR_STORE_PATH, GITHUB_WEB_ORIGIN, githubIssuePath, githubPullPath } from './github-shared.js';
|
|
12
|
+
export { githubPushExecutor, liveGithubExecute, observedPrsToResources, performGithubAction, pullGithubPrs, pushGithubAction, syncGithubFromReal, syncGithubFromRemote, } from './github-connector.js';
|
|
13
|
+
export type { GithubExecute, LiveGithubOptions } from './github-connector.js';
|
|
14
|
+
export { GITHUB_BUDGET_BURST_CEILING, GITHUB_BUDGET_CEILING, GITHUB_BUDGET_MAX_RETRY_AFTER_S, GITHUB_BUDGET_WINDOW_MS, GITHUB_CALL_WEIGHTS, GITHUB_RATE_BUDGET, GithubBudget, GithubBudgetError, githubBudgetPath, githubCallWeight, } from './github-budget.js';
|
|
15
|
+
export type { GithubBudgetErrorKind, GithubBudgetOptions, GithubBudgetReservation, GithubBudgetSnapshot } from './github-budget.js';
|
|
16
|
+
import type { TwinPack } from '@volter/world-core';
|
|
17
|
+
export declare const pack: TwinPack;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// @volter/twin-github — the GitHub PR-evidence twin (REST), on @volter/twin.
|
|
2
|
+
export { applyGithubWrite, githubState, handleGithubRequest, syncPrHeadsToBranchTip, toGithubComment, toGithubIssue, toGithubRest, toGithubReview, toGithubReviewComment } from "./github-twin.js";
|
|
3
|
+
// The REAL git plane: bare repos under the twin's state dir + the git smart-HTTP protocol
|
|
4
|
+
// (clone/fetch/push by an unmodified `git` CLI), reconciled with the Git Data REST surface.
|
|
5
|
+
export { bareRepoDir, ensureBareRepo, hasBareRepo, listGitPlaneRefs, objectExistsInPlane, readGitBlobFromPlane, readGitCommitFromPlane, readGitTagFromPlane, readGitTreeFromPlane, updateGitRefInPlane, writeGitBlobToPlane, writeGitCommitToPlane, writeGitTagToPlane, writeGitTreeToPlane, } from "./github-git-plane.js";
|
|
6
|
+
export { handleGithubGitSmartHttp, parseGitSmartHttpPath, reconcileGitPlaneRefs } from "./github-git-http.js";
|
|
7
|
+
export { createGithubTwinFetch, createGithubTwinServer } from "./github-server.js";
|
|
8
|
+
export { emitGithubEvent } from "./github-events.js";
|
|
9
|
+
// The mirror is a pure frontend (R3): shell + assets + the mounted adapter. Its state comes
|
|
10
|
+
// from the twin's store door, whose projection lives server-side in github-mirror-state.ts.
|
|
11
|
+
export { buildGithubMirrorClient, createGithubMirrorServer, githubMirrorHtml } from "./github-mirror-ui.js";
|
|
12
|
+
export { githubMirrorState } from "./github-mirror-state.js";
|
|
13
|
+
export { GITHUB_MIRROR_STORE, GITHUB_MIRROR_STORE_PATH, GITHUB_WEB_ORIGIN, githubIssuePath, githubPullPath } from "./github-shared.js";
|
|
14
|
+
export { githubPushExecutor, liveGithubExecute, observedPrsToResources, performGithubAction, pullGithubPrs, pushGithubAction, syncGithubFromReal, syncGithubFromRemote, } from "./github-connector.js";
|
|
15
|
+
// The client-side rate budget — the fail-closed backstop `liveGithubExecute` routes every live
|
|
16
|
+
// request through. The MECHANISM is the kernel's shared, vendor-agnostic `RateBudget`; what lives
|
|
17
|
+
// here is GitHub's DECLARATION — and uniquely, its weights ARE GitHub's own published point costs
|
|
18
|
+
// (1 for a read, 5 for a write). Exported so an operator can inspect spend (`snapshot`) and so a
|
|
19
|
+
// caller can catch `GithubBudgetError` by type; there is deliberately no export that disables it.
|
|
20
|
+
export { GITHUB_BUDGET_BURST_CEILING, GITHUB_BUDGET_CEILING, GITHUB_BUDGET_MAX_RETRY_AFTER_S, GITHUB_BUDGET_WINDOW_MS, GITHUB_CALL_WEIGHTS, GITHUB_RATE_BUDGET, GithubBudget, GithubBudgetError, githubBudgetPath, githubCallWeight, } from "./github-budget.js";
|
|
21
|
+
import { registerPack } from '@volter/world-core';
|
|
22
|
+
import { GITHUB_RATE_BUDGET as RATE_BUDGET } from "./github-budget.js";
|
|
23
|
+
import { performGithubAction, syncGithubFromRemote } from "./github-connector.js";
|
|
24
|
+
export const pack = {
|
|
25
|
+
vendor: 'github',
|
|
26
|
+
// 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
|
|
27
|
+
// the real state system: perform one entry against GitHub, refresh the root from it.
|
|
28
|
+
protocol: '2',
|
|
29
|
+
refresh: { every: '5m', webhook: true, onDemand: { atMost: '30s' } },
|
|
30
|
+
stateSystem: { perform: performGithubAction, refresh: syncGithubFromRemote },
|
|
31
|
+
roundTrip: { method: 'POST', path: '/repos/acme/web/issues', body: { title: 'round trip' }, headers: { authorization: 'token round-trip' } },
|
|
32
|
+
// docs/contributing/architecture.md#alias-aware-lookup-at-the-request-boundary: a comment and a review name their issue or PR by `repository` + `number`
|
|
33
|
+
// (an issue and a PR share the `owner/repo#issue:number` id space); when the head adopts GitHub's
|
|
34
|
+
// number for the parent, the kernel resolves these before the comment is performed.
|
|
35
|
+
// shape parity refreshes the twin from itself: github's refresh needs the repo on its origin
|
|
36
|
+
parityOrigin: 'http://twin/repos/acme/web',
|
|
37
|
+
shapeParity: 'held',
|
|
38
|
+
referenceTrip: { method: 'POST', path: '/repos/acme/web/issues/{{number}}/comments', body: { body: 'a comment on the round trip' }, headers: { authorization: 'token round-trip' } },
|
|
39
|
+
references: ['issue_comment', 'review'].map((type) => ({
|
|
40
|
+
type, to: 'issue',
|
|
41
|
+
key: (f) => (f.repository === undefined || f.number === undefined ? undefined : `${String(f.repository)}#issue:${String(f.number)}`),
|
|
42
|
+
adopt: (_f, vendorId) => ({ number: Number(vendorId.split('#issue:').pop()) }),
|
|
43
|
+
})),
|
|
44
|
+
engine: { module: 'github-git-plane', note: 'real bare repositories: git objects live in the plane, the tree references them by sha' },
|
|
45
|
+
// The SAME object github-budget.ts declares at module load — one source of truth, so registering
|
|
46
|
+
// the pack and importing the connector can never arm two different ceilings.
|
|
47
|
+
rateBudget: RATE_BUDGET,
|
|
48
|
+
pullPosture: 'continuous',
|
|
49
|
+
pullPostureReason: "GitHub REST is 5000 req/hour authenticated with X-RateLimit-* headers on every response, so a scheduled sync can measure its own headroom.",
|
|
50
|
+
transport: 'rest',
|
|
51
|
+
archetype: 'engine-control',
|
|
52
|
+
bin: 'world-github',
|
|
53
|
+
resources: ['pull_request', 'pull_request_review', 'pull_request_review_comment', 'issue', 'issue_comment', 'repository', 'branch', 'git_ref'],
|
|
54
|
+
specSource: 'test-fixtures/github-pull-schema.json (full GitHub OpenAPI PR schema)',
|
|
55
|
+
description: 'GitHub PR-evidence REST twin — writes, webhooks, UI mirror.',
|
|
56
|
+
// Adoption + interception, all in the pack's one home (descriptor-first back-migration, adding-a-twin.md §3,
|
|
57
|
+
// 2026-08-31; the eight SDK names, the `@octokit/` scope and the bare `github`/`gh` stems moved
|
|
58
|
+
// off the central maps unchanged). The two canonical Octokit clients plus the official
|
|
59
|
+
// auth/webhook companions — each battery-observed as a repo's ONLY GitHub dep — and the whole
|
|
60
|
+
// `@octokit/` scope so an unlisted satellite still detects. Stems: the bare GITHUB_*/GH_* pair
|
|
61
|
+
// and the credential-var shapes real apps use for their GitHub App / OAuth App connections
|
|
62
|
+
// (BACKEND_GITHUB_*, INF_APP_CONNECTION_GITHUB_APP_*, …_GITHUB_OAUTH_*, …_GITHUB_RADAR_APP_*).
|
|
63
|
+
adoption: {
|
|
64
|
+
// COMMUNITY: GitHub ships no official Python SDK (octokit is JS, go-github is Go), and `PyGithub`
|
|
65
|
+
// is the de-facto Python client of the same REST API.
|
|
66
|
+
pypi: ['pygithub'],
|
|
67
|
+
sdks: [
|
|
68
|
+
'@octokit/rest', '@octokit/core', 'octokit',
|
|
69
|
+
'@octokit/auth-app', '@octokit/auth-oauth-app', '@octokit/auth-oauth-device',
|
|
70
|
+
'@octokit/graphql', '@octokit/webhooks',
|
|
71
|
+
],
|
|
72
|
+
scopes: ['@octokit/'],
|
|
73
|
+
envStems: [
|
|
74
|
+
'GITHUB', 'GH',
|
|
75
|
+
'BACKENDGITHUB', 'INFAPPCONNECTIONGITHUBAPP',
|
|
76
|
+
'INFAPPCONNECTIONGITHUBOAUTH', 'INFAPPCONNECTIONGITHUBRADARAPP',
|
|
77
|
+
// appsmith names its GitHub OAuth app APPSMITH_OAUTH2_GITHUB_CLIENT_ID/_SECRET — the same
|
|
78
|
+
// app-prefixed shape as the INFAPPCONNECTION* stems above (ladder classification 2026-09-02).
|
|
79
|
+
'APPSMITHOAUTH2GITHUB',
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
// The REST API, and on github.com git's smart-HTTP paths (github-git-http.ts serves them: an unmodified
|
|
83
|
+
// `git clone https://github.com/o/r.git` lands in the twin) and the OAuth web flow (src/screens/oauth.tsx: an
|
|
84
|
+
// application's unmodified sign-in with GitHub), a GitHub App's installation page (src/screens/app-installation.tsx), the
|
|
85
|
+
// 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;
|
|
86
|
+
// and on uploads.github.com a release asset's upload.
|
|
87
|
+
// Every other github.com page stays the real site's.
|
|
88
|
+
hosts: [
|
|
89
|
+
{ host: 'api.github.com' },
|
|
90
|
+
// a release's upload_url: its assets are uploaded here (docs.github.com/rest/releases/assets#upload-a-release-asset)
|
|
91
|
+
{ host: 'uploads.github.com', pathPattern: '^/repos/[^/]+/[^/]+/releases/\\d+/assets$' },
|
|
92
|
+
{ host: 'github.com', pathPattern: '^/[^/]+/[^/]+/(info/refs|git-upload-pack|git-receive-pack)$' },
|
|
93
|
+
{ host: 'github.com', pathPattern: '^/login/oauth/(authorize|access_token)$' },
|
|
94
|
+
{ host: 'github.com', pathPattern: '^/apps/[^/]+/installations/new$' },
|
|
95
|
+
{ host: 'github.com', pathPattern: '^/(organizations/[^/]+/)?settings/apps/new$' },
|
|
96
|
+
{ host: 'npm.pkg.github.com', pathPattern: '^/@' },
|
|
97
|
+
],
|
|
98
|
+
};
|
|
99
|
+
// PROTOCOL 2: the pack registers itself on load — its rate budget and its state system (the head
|
|
100
|
+
// resolves `stateSystemFor(vendor)` in the process that serves the twin).
|
|
101
|
+
registerPack(pack);
|