@volter/twin-github 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,112 @@
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 `/api/state` 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, type UiSurface, type UiCompletenessReport } from '@volter/twin-tooling';
18
+
19
+ /** What real GitHub shows on the PR list, PR conversation, and issues list — classified vs the twin. */
20
+ export const GITHUB_UI_INVENTORY: UiSurface[] = [
21
+ // --- rendered: the mirror truly shows these today (proven in the test) ---
22
+ { key: 'prNumber', label: 'PR number (#123)', status: 'rendered' },
23
+ { key: 'title', label: 'PR title', status: 'rendered' },
24
+ { key: 'body', label: 'PR description / body', status: 'rendered' },
25
+ { key: 'stateBadge', label: 'State / merge badge (open/closed/merged)', status: 'rendered' },
26
+ { key: 'labels', label: 'Labels', status: 'rendered' },
27
+ { key: 'assignees', label: 'Assignees', status: 'rendered' },
28
+ { key: 'reviewers', label: 'Reviewers (state + login)', status: 'rendered' },
29
+ { key: 'reviewContent', label: 'Review submissions (body + state)', status: 'rendered' },
30
+ { key: 'comments', label: 'Issue comments / conversation', status: 'rendered' },
31
+ { key: 'reviewComments', label: 'Review (diff-thread) comments', status: 'rendered' },
32
+ { key: 'changedFilesCount', label: 'Changed-files count', status: 'rendered' },
33
+ { key: 'commitsCount', label: 'Commits count', status: 'rendered' },
34
+ { key: 'reviewCount', label: 'Review count', status: 'rendered' },
35
+ { key: 'commentsCount', label: 'Comment count', status: 'rendered' },
36
+ { key: 'baseBranch', label: 'Base branch ref', status: 'rendered' },
37
+ { key: 'headSha', label: 'Head commit sha', status: 'rendered' },
38
+ { key: 'mergeCommit', label: 'Merge commit sha', status: 'rendered' },
39
+ { key: 'mergedAt', label: 'Merged timestamp', status: 'rendered' },
40
+ { key: 'prUrl', label: 'Link to PR on GitHub', status: 'rendered' },
41
+ { key: 'issuesList', label: 'Issues list (first-class issues view)', status: 'rendered' },
42
+ { key: 'repoSelector', label: 'Per-repo selector (repo grouping)', status: 'rendered' },
43
+ // reviewCommentsCount is computed + emitted in /api/state and now rendered in the
44
+ // conversation header alongside reviewCount + commentsCount.
45
+ { key: 'reviewCommentsCount', label: 'Review-comment (diff-thread) count', status: 'rendered' },
46
+ // CI rollup (commit statuses + check-runs) over the PR head_sha — the twin now models
47
+ // statuses[]/checkRuns[]; the mirror renders a ✓/✗/● indicator + a Checks side-section.
48
+ { key: 'checksStatus', label: 'CI / status checks', status: 'rendered' },
49
+ // milestone: the twin models milestone NUMBER + state.milestones; the mirror renders
50
+ // the milestone title as a badge on PR/issue rows + a Milestone side-section.
51
+ { key: 'milestone', label: 'Milestone', status: 'rendered' },
52
+ // linked PRs/issues: the twin models issue.linked[]; the mirror renders typed
53
+ // cross-reference links on the issue rows.
54
+ { key: 'linkedIssues', label: 'Linked / source issue', status: 'rendered' },
55
+ // requested reviewers (distinct from submitted reviews): the twin models
56
+ // requested_reviewers[]; the mirror renders them as chips in the Reviewers side-section.
57
+ { key: 'requestedReviewers', label: 'Requested reviewers', status: 'rendered' },
58
+ // per-file diffs: the twin models pr.files (GithubPullFile) on LOCAL writes; the mirror
59
+ // renders the conversation's Files-changed section (filename + status + +/- counts).
60
+ // Empty for observed-only PRs (no per-file evidence) — honesty preserved.
61
+ { key: 'filesChangedContent', label: 'Per-file diff list (filenames + status + counts)', status: 'rendered' },
62
+ // additions/deletions: the twin rolls these up from the carried file list on LOCAL writes;
63
+ // the mirror renders them in the Aside snapshot counts. Undefined for observed-only PRs.
64
+ { key: 'diffStats', label: 'Additions / deletions line counts', status: 'rendered' },
65
+ // per-commit detail: the twin models pr.commits_list (GithubPullCommit) on LOCAL writes;
66
+ // the mirror renders the conversation's Commits section (sha + message + author + date).
67
+ // Empty for observed-only PRs (only a commit COUNT is observed) — honesty preserved.
68
+ { key: 'commitsContent', label: 'Per-commit list (sha + message + author)', status: 'rendered' },
69
+
70
+ // --- Actions (workflows + runs + jobs) — a LOCAL construct (running real CI is the only
71
+ // Actions non-goal; the run/job OBJECTS are modeled + rendered in the Actions view) ---
72
+ // the Actions tab lists workflow runs; each run row carries a status/conclusion badge.
73
+ { key: 'actionsRunsList', label: 'Workflow runs list (Actions view)', status: 'rendered' },
74
+ // the run badge reflects status (queued/in_progress/completed) + conclusion (success/
75
+ // failure/cancelled), rendered as a glyph + label on every run row + the detail header.
76
+ { key: 'runStatusBadge', label: 'Run status / conclusion badge', status: 'rendered' },
77
+ // selecting a run renders a detail pane with its jobs + each job's steps.
78
+ { key: 'runJobs', label: 'Run detail — jobs + steps', status: 'rendered' },
79
+
80
+ // --- Releases (workflows of the repo's release channel) — a LOCAL construct (no observed
81
+ // release evidence; release ASSET binary BYTES are the only Releases non-goal) ---
82
+ // the Releases tab lists releases; each row carries tag + name + draft/prerelease badge.
83
+ { key: 'releasesList', label: 'Releases list (Releases view)', status: 'rendered' },
84
+ // the channel badge reflects draft / prerelease / published, rendered on every row + header.
85
+ { key: 'releaseChannelBadge', label: 'Release channel badge (draft/prerelease/release)', status: 'rendered' },
86
+ // selecting a release renders a detail pane with its notes + ASSET catalog (metadata only).
87
+ { key: 'releaseAssets', label: 'Release detail — asset catalog (name/type/size)', status: 'rendered' },
88
+
89
+ // --- Discussions (a LOCAL construct; modeled over REST — a declared deviation from
90
+ // GitHub's GraphQL Discussions API. No observed discussion evidence) ---
91
+ // the Discussions tab lists discussions; each row carries title + category + answer badge.
92
+ { key: 'discussionsList', label: 'Discussions list (Discussions view)', status: 'rendered' },
93
+ // the category chip reflects the discussion's category (Q&A categories are answerable).
94
+ { key: 'discussionCategory', label: 'Discussion category chip (General/Q&A/…)', status: 'rendered' },
95
+ // an answerable (Q&A) discussion shows an answered/unanswered badge.
96
+ { key: 'discussionAnswerStatus', label: 'Discussion answered/unanswered status (Q&A)', status: 'rendered' },
97
+ // selecting a discussion renders a detail pane with the opening post + the comment/reply
98
+ // thread, highlighting the accepted answer.
99
+ { key: 'discussionThread', label: 'Discussion detail — comment/reply thread + accepted answer', status: 'rendered' },
100
+
101
+ // --- unmodeled: real GitHub shows it, the evidence twin models it for neither
102
+ // observed nor local writes (a genuine twin-model gap) ---
103
+ // The twin never carries author identity on a PR (observed evidence has only repo/number/
104
+ // counts; local writes don't capture the acting login into the PR object either).
105
+ { key: 'author', label: 'PR author (login + avatar)', status: 'unmodeled' },
106
+ // The twin tracks base_ref + head_sha but not the head branch NAME (only the head sha).
107
+ { key: 'headBranch', label: 'Head branch ref name', status: 'unmodeled' },
108
+ ];
109
+
110
+ export function githubUiConformance(): UiCompletenessReport {
111
+ return checkUiCompleteness('github', GITHUB_UI_INVENTORY);
112
+ }
@@ -0,0 +1,256 @@
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 a state badge, 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, type UiStructureCheck, type UiStructureReport } from '@volter/twin-tooling';
15
+ import {
16
+ PrList, Conversation, Aside, IssuesTable, CiStatus, ActionsView, ReleasesView, DiscussionsView, DeploymentsView,
17
+ type PullRequest, type Issue, type Review, type Commit, type Comment, type ChangedFile,
18
+ type WorkflowRun, type Job, type Release, type Discussion, type Deployment, type Environment,
19
+ } from '../client/github-mirror.tsx';
20
+
21
+ // ── Seeded props: one rich PR (reviews/comments/labels/assignees/CI/milestone/files)
22
+ // plus a second PR so the list has more than one row, and one issue. ───────────────
23
+ const PR: PullRequest = {
24
+ repo: 'o/r', number: 1, title: 'Add feature', body: 'why this PR', state: 'open',
25
+ author: 'octocat', baseBranch: 'main', headBranch: 'feat', headSha: 'abcdef1234567',
26
+ createdAt: '2026-04-01T00:00:00Z', updatedAt: '2026-04-02T00:00:00Z',
27
+ additions: 42, deletions: 7, changedFiles: 2, commitsCount: 3,
28
+ commentsCount: 1, reviewCount: 1, reviewCommentsCount: 1,
29
+ labels: [{ name: 'bug' }, { name: 'enhancement' }],
30
+ assignees: ['alice'], requestedReviewers: ['carol'], milestone: 'v1.0',
31
+ ciStatus: 'success',
32
+ };
33
+ const PR_TWO: PullRequest = {
34
+ repo: 'o/r', number: 2, title: 'Fix bug', state: 'open', author: 'hubot',
35
+ createdAt: '2026-03-01T00:00:00Z', updatedAt: '2026-03-02T00:00:00Z', ciStatus: 'failure',
36
+ };
37
+ const ISSUE: Issue = {
38
+ repo: 'o/web', number: 1, title: 'An issue', state: 'open',
39
+ createdAt: '2026-04-04T00:00:00Z', updatedAt: '2026-04-04T00:00:00Z',
40
+ labels: [{ name: 'triage' }], assignees: ['bob'], milestone: 'v1.0',
41
+ };
42
+ const ISSUE_TWO: Issue = {
43
+ repo: 'o/web', number: 2, title: 'Another issue', state: 'closed',
44
+ createdAt: '2026-04-05T00:00:00Z', updatedAt: '2026-04-05T00:00:00Z',
45
+ };
46
+ const REVIEWS: Review[] = [{ repo: 'o/r', number: 1, reviewer: 'carol', state: 'approved', body: 'looks good', submittedAt: '2026-04-02T01:00:00Z' }];
47
+ const COMMITS: Commit[] = [{ repo: 'o/r', number: 1, sha: 'abcdef1234567', message: 'do the thing', authorLogin: 'octocat', date: '2026-04-01T12:00:00Z' }];
48
+ const COMMENTS: Comment[] = [{ repo: 'o/r', number: 1, author: 'dave', body: 'a note', createdAt: '2026-04-02T02:00:00Z' }];
49
+ const REVIEW_COMMENTS: Comment[] = [{ repo: 'o/r', number: 1, author: 'erin', body: 'inline diff note', path: 'src/x.ts', createdAt: '2026-04-02T03:00:00Z' }];
50
+ const FILES: ChangedFile[] = [{ repo: 'o/r', number: 1, filename: 'src/x.ts', status: 'modified', additions: 42, deletions: 7 }];
51
+ // Actions: two runs (one completed/success, one cancelled) so the runs list has >1 row, plus
52
+ // a job (with steps) for the first run so the run-detail jobs section renders.
53
+ const RUNS: WorkflowRun[] = [
54
+ { 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' },
55
+ { repo: 'o/r', id: 102, workflowId: 1, workflowName: 'CI', runNumber: 2, event: 'workflow_dispatch', status: 'completed', conclusion: 'cancelled', headSha: 'abcdef7654321', headBranch: 'dev', jobCount: 0 },
56
+ ];
57
+ const JOBS: Job[] = [{ repo: 'o/r', id: 201, runId: 101, name: 'build', status: 'completed', conclusion: 'success', steps: [{ name: 'set up job', status: 'completed', conclusion: 'success', number: 1 }] }];
58
+ // Releases: two releases (one published with an asset, one draft) so the list has >1 row and
59
+ // the detail pane renders the asset catalog (metadata only — bytes are the Non-goal).
60
+ const RELEASES: Release[] = [
61
+ { 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 }] },
62
+ { 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: [] },
63
+ ];
64
+ // Discussions: two discussions (one answerable Q&A with a chosen answer + a reply, one
65
+ // General) so the list has >1 row and the detail pane renders the comment thread + the
66
+ // accepted-answer landmark.
67
+ const DISCUSSIONS: Discussion[] = [
68
+ {
69
+ repo: 'o/r', number: 1, title: 'How do I configure X?', body: 'I am stuck on X.', state: 'open',
70
+ categorySlug: 'q-a', categoryName: 'Q&A', categoryEmoji: '🙏', isAnswerable: true,
71
+ answerCommentId: 101, isAnswered: true, commentsCount: 2, updatedAt: '2026-04-12T00:00:00Z',
72
+ url: 'https://github.com/o/r/discussions/1',
73
+ comments: [
74
+ { id: 101, parentId: null, body: 'Do it this way.', isAnswer: true, createdAt: '2026-04-12T01:00:00Z' },
75
+ { id: 102, parentId: 101, body: 'Thanks, that worked!', isAnswer: false, createdAt: '2026-04-12T02:00:00Z' },
76
+ ],
77
+ },
78
+ {
79
+ repo: 'o/r', number: 2, title: 'Welcome!', body: 'Say hi here.', state: 'open',
80
+ categorySlug: 'general', categoryName: 'General', categoryEmoji: '💬', isAnswerable: false,
81
+ answerCommentId: null, isAnswered: false, commentsCount: 0, updatedAt: '2026-04-13T00:00:00Z', comments: [],
82
+ },
83
+ ];
84
+
85
+ // Deployments: two deployments (one success in production, one pending in staging) so the
86
+ // list has >1 row, plus two environments (one protected with a wait timer + reviewers, one
87
+ // unprotected) so the environments list renders with a protection summary.
88
+ const DEPLOYMENTS: Deployment[] = [
89
+ { 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' }] },
90
+ { repo: 'o/r', id: 2, ref: 'dev', sha: 'fedcba7654321', environment: 'staging', task: 'deploy', state: 'pending', createdAt: '2026-04-11T00:00:00Z', statuses: [] },
91
+ ];
92
+ const ENVIRONMENTS: Environment[] = [
93
+ { repo: 'o/r', name: 'production', waitTimer: 30, reviewers: ['alice', 'bob'], protected: true },
94
+ { repo: 'o/r', name: 'staging', reviewers: [], protected: false },
95
+ ];
96
+
97
+ /** Count non-overlapping occurrences of a substring in `html`. */
98
+ function count(html: string, marker: string): number {
99
+ if (!marker) return 0;
100
+ let n = 0;
101
+ let i = html.indexOf(marker);
102
+ while (i !== -1) { n += 1; i = html.indexOf(marker, i + marker.length); }
103
+ return n;
104
+ }
105
+
106
+ /** Render the exported presentational components to static HTML, once, deterministically. */
107
+ export function renderGithubMirrorMarkup() {
108
+ return {
109
+ prList: renderToStaticMarkup(createElement(PrList, { prs: [PR, PR_TWO], reviews: REVIEWS, selectedKey: 'o/r#1' })),
110
+ conversation: renderToStaticMarkup(createElement(Conversation, { pr: PR, reviews: REVIEWS, commits: COMMITS, comments: COMMENTS, reviewComments: REVIEW_COMMENTS, files: FILES })),
111
+ aside: renderToStaticMarkup(createElement(Aside, { pr: PR, reviews: REVIEWS, commits: COMMITS, files: FILES })),
112
+ issues: renderToStaticMarkup(createElement(IssuesTable, { issues: [ISSUE, ISSUE_TWO] })),
113
+ ciStatus: renderToStaticMarkup(createElement(CiStatus, { status: 'success' })),
114
+ actions: renderToStaticMarkup(createElement(ActionsView, { runs: RUNS, jobs: JOBS, selectedRunId: 101 })),
115
+ releases: renderToStaticMarkup(createElement(ReleasesView, { releases: RELEASES, selectedReleaseId: 1 })),
116
+ discussions: renderToStaticMarkup(createElement(DiscussionsView, { discussions: DISCUSSIONS, selectedKey: 'o/r#1' })),
117
+ deployments: renderToStaticMarkup(createElement(DeploymentsView, { deployments: DEPLOYMENTS, environments: ENVIRONMENTS })),
118
+ };
119
+ }
120
+
121
+ /** The structural checklist: each `present` is derived from the rendered markup above. */
122
+ export function githubUiStructureChecks(): UiStructureCheck[] {
123
+ const dom = renderGithubMirrorMarkup();
124
+ return [
125
+ {
126
+ key: 'pr-list-row-per-pr',
127
+ label: 'the PR list emits a row (.pr-row) per pull request',
128
+ present: count(dom.prList, 'class="pr-row') === 2,
129
+ },
130
+ {
131
+ key: 'pr-row-state-badge',
132
+ label: 'each PR row shows a state badge element (.state-badge)',
133
+ present: count(dom.prList, 'class="state-badge') >= 2,
134
+ },
135
+ {
136
+ key: 'pr-row-ci-status',
137
+ label: 'a PR row renders a CI status indicator (.ci-status)',
138
+ present: dom.prList.includes('class="ci-status'),
139
+ },
140
+ {
141
+ key: 'conversation-detail-pane',
142
+ label: 'the conversation/detail pane renders (section.conversation with a heading + state line)',
143
+ present: dom.conversation.includes('class="conversation"') && dom.conversation.includes('class="state-line"'),
144
+ },
145
+ {
146
+ key: 'conversation-comments-timeline',
147
+ label: 'the conversation pane has a comments timeline (.timeline-item with a .comment)',
148
+ present: dom.conversation.includes('class="timeline-item"') && dom.conversation.includes('class="comment"'),
149
+ },
150
+ {
151
+ key: 'conversation-reviews-section',
152
+ label: 'the conversation pane has a reviews section (.review-card)',
153
+ present: dom.conversation.includes('class="review-card"'),
154
+ },
155
+ {
156
+ key: 'conversation-files-changed',
157
+ label: 'the conversation pane has a files-changed section (.file-list with a .file-row)',
158
+ present: dom.conversation.includes('class="file-list"') && dom.conversation.includes('class="file-row"'),
159
+ },
160
+ {
161
+ key: 'aside-sidebar',
162
+ label: 'the detail Aside renders as a sidebar landmark (aside.sidebar)',
163
+ present: dom.aside.includes('<aside class="sidebar"'),
164
+ },
165
+ {
166
+ key: 'aside-reviewers-section',
167
+ label: 'the Aside has a Reviewers section',
168
+ present: dom.aside.includes('<h3>Reviewers</h3>'),
169
+ },
170
+ {
171
+ key: 'aside-labels-assignees-milestone',
172
+ label: 'the Aside has Labels, Assignees and Milestone sections',
173
+ present: dom.aside.includes('<h3>Labels</h3>') && dom.aside.includes('<h3>Assignees</h3>') && dom.aside.includes('<h3>Milestone</h3>'),
174
+ },
175
+ {
176
+ key: 'ci-status-element',
177
+ label: 'the CI status component renders a labelled status element (.ci-status.success)',
178
+ present: dom.ciStatus.includes('class="ci-status success"') && dom.ciStatus.includes('title="checks: success"'),
179
+ },
180
+ {
181
+ key: 'issues-row-per-issue',
182
+ label: 'the issues view emits a row (.issue-row) per issue',
183
+ present: count(dom.issues, 'issue-row') === 2,
184
+ },
185
+ {
186
+ key: 'actions-run-row-per-run',
187
+ label: 'the Actions view emits a run row (.run-row) per workflow run',
188
+ present: count(dom.actions, 'class="run-row') === 2,
189
+ },
190
+ {
191
+ key: 'actions-run-status-badge',
192
+ label: 'each run row shows a run status/conclusion badge (.run-badge)',
193
+ present: count(dom.actions, 'class="run-badge') >= 2,
194
+ },
195
+ {
196
+ key: 'actions-run-detail-jobs',
197
+ label: 'the run detail pane renders a jobs section (.job-list with a .job-row + .step-row)',
198
+ present: dom.actions.includes('class="job-list"') && dom.actions.includes('class="job-row"') && dom.actions.includes('class="step-row"'),
199
+ },
200
+ {
201
+ key: 'releases-row-per-release',
202
+ label: 'the Releases view emits a release row (.release-row) per release',
203
+ present: count(dom.releases, 'class="release-row') === 2,
204
+ },
205
+ {
206
+ key: 'releases-channel-badge',
207
+ label: 'each release row shows a draft/prerelease/release channel badge (.release-badge)',
208
+ present: count(dom.releases, 'class="release-badge') >= 2,
209
+ },
210
+ {
211
+ key: 'releases-detail-assets',
212
+ label: 'the release detail pane renders an asset catalog (.asset-list with an .asset-row)',
213
+ present: dom.releases.includes('class="asset-list"') && dom.releases.includes('class="asset-row"'),
214
+ },
215
+ {
216
+ key: 'discussions-row-per-discussion',
217
+ label: 'the Discussions view emits a discussion row (.discussion-row) per discussion',
218
+ present: count(dom.discussions, 'class="discussion-row') === 2,
219
+ },
220
+ {
221
+ key: 'discussions-category-badge',
222
+ label: 'each discussion row shows a category chip (.discussion-category)',
223
+ present: count(dom.discussions, 'class="discussion-category') >= 2,
224
+ },
225
+ {
226
+ key: 'discussions-answer-badge',
227
+ label: 'an answerable discussion shows an answered/unanswered badge (.answer-badge)',
228
+ present: dom.discussions.includes('class="answer-badge'),
229
+ },
230
+ {
231
+ key: 'discussions-detail-thread',
232
+ label: 'the discussion detail pane renders a comment thread (.comment-thread with a .thread-comment) + the accepted answer (.accepted-answer)',
233
+ present: dom.discussions.includes('class="comment-thread"') && dom.discussions.includes('class="thread-comment') && dom.discussions.includes('class="comment accepted-answer"'),
234
+ },
235
+ {
236
+ key: 'deployments-row-per-deployment',
237
+ label: 'the Deployments view emits a deployment row (.deployment-row) per deployment',
238
+ present: count(dom.deployments, 'class="deployment-row') === 2,
239
+ },
240
+ {
241
+ key: 'deployments-state-badge',
242
+ label: 'each deployment row shows a state badge (.deployment-state)',
243
+ present: count(dom.deployments, 'class="deployment-state') >= 2,
244
+ },
245
+ {
246
+ key: 'deployments-environments-list',
247
+ label: 'the Deployments view renders an environments list (.environments-list with an .environment-row + a protection summary)',
248
+ present: dom.deployments.includes('class="environments-list"') && dom.deployments.includes('class="environment-row"') && dom.deployments.includes('class="environment-protected'),
249
+ },
250
+ ];
251
+ }
252
+
253
+ /** RUNG-5 structural DOM report for the GitHub mirror. Read-only, deterministic. */
254
+ export function githubUiStructure(): UiStructureReport {
255
+ return checkUiStructure('github', githubUiStructureChecks());
256
+ }
package/src/index.ts ADDED
@@ -0,0 +1,23 @@
1
+ // @volter/twin-github — the GitHub PR-evidence twin (REST), on @volter/twin.
2
+ export { applyGithubWrite, githubState, handleGithubRequest, toGithubComment, toGithubIssue, toGithubRest, toGithubReview, toGithubReviewComment } from './github-twin.ts';
3
+ export type { GithubComment, GithubIssue, GithubPr, GithubResponse, GithubReview, GithubState, GithubWriteEvent, GithubWriteOutcome } from './github-twin.ts';
4
+ export { createGithubTwinServer } from './github-server.ts';
5
+ export { clearGithubWebhooks, emitGithubEvent, listGithubWebhooks, registerGithubWebhook } from './github-events.ts';
6
+ export type { GithubWebhook, GithubWebhookDelivery } from './github-events.ts';
7
+ export { buildGithubMirrorClient, createGithubMirrorServer, githubMirrorHtml, githubMirrorState } from './github-mirror-ui.ts';
8
+ export {
9
+ liveGithubExecute, observedPrsToResources, pullGithubPrs, pushGithubAction,
10
+ pushPendingGithubActions, syncGithubFromReal,
11
+ } from './github-connector.ts';
12
+ export type { GithubExecute } from './github-connector.ts';
13
+
14
+ // Registry descriptor (#1).
15
+ import type { TwinPack } from '@volter/twin';
16
+ export const pack: TwinPack = {
17
+ vendor: 'github',
18
+ transport: 'rest',
19
+ bin: 'world-github',
20
+ resources: ['pull_request', 'pull_request_review', 'pull_request_review_comment', 'issue', 'issue_comment'],
21
+ specSource: 'test-fixtures/github-pull-schema.json (full GitHub OpenAPI PR schema)',
22
+ description: 'GitHub PR-evidence REST twin — writes, webhooks, UI mirror.',
23
+ };