@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
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
// NOT declared fails CI. The companion coverage report says what we emit vs what the
|
|
9
9
|
// vendor declares. Standing gate: wrong types, undeclared omissions, fabricated
|
|
10
10
|
// surface, or enum violations all fail.
|
|
11
|
-
import {
|
|
11
|
+
import { readFile } from 'node:fs/promises';
|
|
12
|
+
import { specConformance } from '@volter/world-tooling';
|
|
12
13
|
import { githubState, toGithubComment, toGithubRest, toGithubReview } from './github-twin.ts';
|
|
13
14
|
|
|
14
15
|
type JsonSchema = specConformance.JsonSchema;
|
|
@@ -29,22 +30,22 @@ const fixturePath = (name: string): string => new URL(`../test-fixtures/${name}`
|
|
|
29
30
|
|
|
30
31
|
/** Load the vendored full GitHub pull-request JSON Schema (dereferenced OpenAPI). */
|
|
31
32
|
export async function loadGithubPrSchema(): Promise<JsonSchema> {
|
|
32
|
-
return JSON.parse(await
|
|
33
|
+
return JSON.parse(await readFile(fixturePath('github-pull-schema.json'), 'utf8')) as JsonSchema;
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
/** Load the vendored GitHub pull-request-review JSON Schema. */
|
|
36
37
|
export async function loadGithubReviewSchema(): Promise<JsonSchema> {
|
|
37
|
-
return JSON.parse(await
|
|
38
|
+
return JSON.parse(await readFile(fixturePath('github-review-schema.json'), 'utf8')) as JsonSchema;
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
/** Load the vendored GitHub issue-comment JSON Schema. */
|
|
41
42
|
export async function loadGithubCommentSchema(): Promise<JsonSchema> {
|
|
42
|
-
return JSON.parse(await
|
|
43
|
+
return JSON.parse(await readFile(fixturePath('github-comment-schema.json'), 'utf8')) as JsonSchema;
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
/** Load the declared evidence-twin scope (fields deliberately not modeled, + reasons). */
|
|
46
47
|
export async function loadGithubKnownDeviations(): Promise<KnownDeviation[]> {
|
|
47
|
-
const doc = JSON.parse(await
|
|
48
|
+
const doc = JSON.parse(await readFile(fixturePath('github-known-deviations.json'), 'utf8')) as { deviations: KnownDeviation[] };
|
|
48
49
|
return doc.deviations;
|
|
49
50
|
}
|
|
50
51
|
|
|
@@ -58,14 +59,18 @@ export function checkGithubConformance(schema: JsonSchema, opts: { root?: string
|
|
|
58
59
|
const violations: GithubViolation[] = [];
|
|
59
60
|
let fieldsChecked = 0;
|
|
60
61
|
let knownIgnored = 0;
|
|
61
|
-
|
|
62
|
+
// "Every PR the twin SERVES" — which is the rows marked as pull-request evidence, not the
|
|
63
|
+
// counter shells `ensure()` materializes for issue numbers. Checking a shell measures an
|
|
64
|
+
// object no door answers with, and its emptiness would read as the twin's conformance.
|
|
65
|
+
const served = state.prs.filter((p) => p.is_pull_request);
|
|
66
|
+
for (const pr of served) {
|
|
62
67
|
const rest = toGithubRest(pr);
|
|
63
68
|
const rep = specConformance.checkSpecConformance(rest, schema, { exemptKey: isTwinExtra, known: opts.known ?? [] });
|
|
64
69
|
fieldsChecked += rep.fieldsChecked;
|
|
65
70
|
knownIgnored += rep.knownIgnored;
|
|
66
71
|
for (const v of rep.violations) violations.push({ ...v, prId: pr.id });
|
|
67
72
|
}
|
|
68
|
-
return { ok: violations.length === 0, prsChecked:
|
|
73
|
+
return { ok: violations.length === 0, prsChecked: served.length, fieldsChecked, violations, knownIgnored };
|
|
69
74
|
}
|
|
70
75
|
|
|
71
76
|
export type GithubCoverageReport = specConformance.SpecCoverageReport;
|
package/src/github-connector.ts
CHANGED
|
@@ -18,16 +18,11 @@
|
|
|
18
18
|
// text it didn't receive; the one excluded thing is the Non-goal — actual repository
|
|
19
19
|
// file CONTENTS (git blob bytes) — which the pull does not fetch. Content-bearing twin
|
|
20
20
|
// state thus comes from BOTH the observed fold and LOCAL writes, which push reconciles.
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
// carries its inline (diff-anchored) comments, since GitHub wraps every inline comment in a
|
|
27
|
-
// review — `pull_request_review_id` on each `/pulls/{n}/comments` row is the join.
|
|
28
|
-
import { appendEvent, assertBudgetGuardIntact, buildShadowState, confirmAction, pendingActions, syncPull } from '@volter/twin';
|
|
29
|
-
import type { RemoteExecute, SyncResource, WorldServiceEvent } from '@volter/twin';
|
|
30
|
-
import { GithubBudget, githubCallWeight, type GithubBudgetOptions } from './github-budget.ts';
|
|
21
|
+
import { assertBudgetGuardIntact } from '@volter/world-core';
|
|
22
|
+
import { observeResource, toEntry } from '@volter/world-core';
|
|
23
|
+
import type { PerformContext, PushOutcome, RemoteExecute, SyncResource, TwinAction, WorldServiceEvent } from '@volter/world-core';
|
|
24
|
+
import { twinResources, ownFields } from '@volter/world-core';
|
|
25
|
+
import { GithubBudget, GithubBudgetError, githubCallWeight, type GithubBudgetOptions } from './github-budget.ts';
|
|
31
26
|
import { githubState } from './github-twin.ts';
|
|
32
27
|
|
|
33
28
|
const SERVICE = 'github';
|
|
@@ -149,7 +144,14 @@ export function liveGithubExecute(
|
|
|
149
144
|
// first either way, so the refusal survives the throw. GitHub answers a tripped SECONDARY
|
|
150
145
|
// limit with 403 + `Retry-After` rather than 429, which is why the header is read on every
|
|
151
146
|
// response and not only on a 429.
|
|
152
|
-
|
|
147
|
+
// recordCall may THROW after arming the cooldown (a back-off beyond the cap). On a refused
|
|
148
|
+
// call that louder refusal wins; an answer GitHub ACCEPTED is kept, so a write that landed is
|
|
149
|
+
// never recorded as failed and performed again on retry.
|
|
150
|
+
try {
|
|
151
|
+
budget.recordCall(weight, resHeaders, { status: res.status, reservation });
|
|
152
|
+
} catch (error) {
|
|
153
|
+
if (!(error instanceof GithubBudgetError) || !res.ok) throw error;
|
|
154
|
+
}
|
|
153
155
|
return { status: res.status, data };
|
|
154
156
|
},
|
|
155
157
|
};
|
|
@@ -226,7 +228,8 @@ type ObservedPr = {
|
|
|
226
228
|
headSha?: string;
|
|
227
229
|
changedFiles?: number;
|
|
228
230
|
commitsCount?: number;
|
|
229
|
-
authorLogin?: string;
|
|
231
|
+
authorLogin?: string | null;
|
|
232
|
+
authorType?: string;
|
|
230
233
|
/** The PR's own `updated_at` — the provider instant that dates its delta AND the stamp
|
|
231
234
|
* the next pull compares against to decide whether its conversation is worth buying. */
|
|
232
235
|
updatedAt?: string;
|
|
@@ -279,6 +282,8 @@ type ObservedIssue = {
|
|
|
279
282
|
title?: string;
|
|
280
283
|
body?: string;
|
|
281
284
|
state?: string;
|
|
285
|
+
created_at?: string;
|
|
286
|
+
updated_at?: string;
|
|
282
287
|
};
|
|
283
288
|
|
|
284
289
|
/**
|
|
@@ -476,7 +481,9 @@ export async function pullGithubPrs(
|
|
|
476
481
|
});
|
|
477
482
|
pr.commentCount = commentNodes.length;
|
|
478
483
|
}
|
|
479
|
-
if (n.user
|
|
484
|
+
if (n.user === null) pr.authorLogin = null;
|
|
485
|
+
else if (n.user?.login !== undefined && n.user?.login !== null) pr.authorLogin = String(n.user.login);
|
|
486
|
+
if (n.user?.type !== undefined && n.user?.type !== null) pr.authorType = String(n.user.type);
|
|
480
487
|
// CONTENT: title/body/state are real text the REST PR object returns — fold them.
|
|
481
488
|
if (n.title !== undefined && n.title !== null) pr.title = String(n.title);
|
|
482
489
|
if (n.body !== undefined && n.body !== null) pr.body = String(n.body);
|
|
@@ -526,6 +533,9 @@ export async function pullGithubIssues(
|
|
|
526
533
|
if (n.title !== undefined && n.title !== null) iss.title = String(n.title);
|
|
527
534
|
if (n.body !== undefined && n.body !== null) iss.body = String(n.body);
|
|
528
535
|
if (n.state !== undefined && n.state !== null) iss.state = String(n.state);
|
|
536
|
+
// the timestamps the write handler stores: shape parity between a written and an observed issue
|
|
537
|
+
if (typeof n.created_at === 'string') iss.created_at = n.created_at;
|
|
538
|
+
if (typeof n.updated_at === 'string') iss.updated_at = n.updated_at;
|
|
529
539
|
out.push(iss);
|
|
530
540
|
}
|
|
531
541
|
return out;
|
|
@@ -559,7 +569,8 @@ export function observedPrsToResources(prs: ObservedPr[]): SyncResource[] {
|
|
|
559
569
|
// WHO opened it, and when GitHub last saw it change — the stamp the next pull's budget
|
|
560
570
|
// compares against, kept on the subject rather than in a cache beside it.
|
|
561
571
|
if (p.authorLogin !== undefined) fields.authorLogin = p.authorLogin;
|
|
562
|
-
if (p.
|
|
572
|
+
if (p.authorType !== undefined) fields.user_type = p.authorType;
|
|
573
|
+
if (p.updatedAt !== undefined) { fields.updatedAt = p.updatedAt; fields.observedUpdatedAt = p.updatedAt; }
|
|
563
574
|
// The reviewers' WORDS ride the observation: how many reviews, and the newest one's
|
|
564
575
|
// verdict and body, so a delta on the feed says what a reviewer said (a consumer decides
|
|
565
576
|
// from the words, never from a bare count moving). The reviews themselves are subjects
|
|
@@ -649,11 +660,11 @@ export function observedPrsToResources(prs: ObservedPr[]): SyncResource[] {
|
|
|
649
660
|
* conversation gets bought.
|
|
650
661
|
*/
|
|
651
662
|
export function lastObservedPrUpdates(root?: string): Record<string, string> {
|
|
652
|
-
const shadow = buildShadowState(SERVICE, () => null, root);
|
|
653
663
|
const out: Record<string, string> = {};
|
|
654
|
-
for (const
|
|
664
|
+
for (const resource of twinResources(SERVICE, root)) {
|
|
665
|
+
const subject = { subject: resource, fields: ownFields(resource) };
|
|
655
666
|
if (subject.subject.type !== 'pull_request') continue;
|
|
656
|
-
const updatedAt = subject.fields.updatedAt;
|
|
667
|
+
const updatedAt = subject.fields.observedUpdatedAt ?? subject.fields.updatedAt;
|
|
657
668
|
if (typeof updatedAt === 'string' && updatedAt !== '') out[subject.subject.id] = updatedAt;
|
|
658
669
|
}
|
|
659
670
|
return out;
|
|
@@ -666,6 +677,8 @@ export function observedIssuesToResources(issues: ObservedIssue[]): SyncResource
|
|
|
666
677
|
if (i.title !== undefined) fields.title = i.title;
|
|
667
678
|
if (i.body !== undefined) fields.body = i.body;
|
|
668
679
|
if (i.state !== undefined) fields.state = i.state;
|
|
680
|
+
if (i.created_at !== undefined) fields.created_at = i.created_at;
|
|
681
|
+
if (i.updated_at !== undefined) fields.updated_at = i.updated_at;
|
|
669
682
|
return { type: 'issue', id: i.id, fields };
|
|
670
683
|
});
|
|
671
684
|
}
|
|
@@ -698,14 +711,13 @@ type AppendOutcome = { appended: boolean; conflictId?: string };
|
|
|
698
711
|
// resources in the same pull, so it must NOT abort the whole repo's fold: skip the row,
|
|
699
712
|
// surface its stable id, keep folding. Every OTHER error stays fatal (re-thrown).
|
|
700
713
|
function appendTolerant(event: WorldServiceEvent, root?: string): AppendOutcome {
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
}
|
|
714
|
+
// PROTOCOL 2: the pack OBSERVES a resource; the kernel diffs it against the tree and folds what
|
|
715
|
+
// changed onto the root's log (observe.ts). The v1 row shape is read for its subject and fields.
|
|
716
|
+
const entry = toEntry(event as unknown as Record<string, unknown>);
|
|
717
|
+
// Observations are field patches. Copying the pre-refresh tree here would reintroduce
|
|
718
|
+
// stale fields after earlier observations in the same collected batch update them.
|
|
719
|
+
const report = observeResource(event.service, { type: entry.subject.type, id: entry.subject.id, fields: entry.fields ?? {} }, { ...(root !== undefined ? { root } : {}), at: entry.occurredAt });
|
|
720
|
+
return { appended: report.appended > 0 || report.observed > 0 && report.unchanged === 0 && report.appended === 0 };
|
|
709
721
|
}
|
|
710
722
|
|
|
711
723
|
// A content-hashed, deterministic id for a folded evidence snapshot — never random,
|
|
@@ -736,6 +748,8 @@ function foldObservedPr(pr: ObservedPr, occurredAt: string, root?: string): Appe
|
|
|
736
748
|
},
|
|
737
749
|
};
|
|
738
750
|
if (pr.title !== undefined) data.title = pr.title;
|
|
751
|
+
if (pr.authorLogin !== undefined) data.user_login = pr.authorLogin;
|
|
752
|
+
if (pr.authorType !== undefined) data.user_type = pr.authorType;
|
|
739
753
|
if (pr.body !== undefined) data.body = pr.body;
|
|
740
754
|
if (pr.state !== undefined) data.state = pr.state;
|
|
741
755
|
if (pr.draft !== undefined) data.draft = pr.draft;
|
|
@@ -745,7 +759,7 @@ function foldObservedPr(pr: ObservedPr, occurredAt: string, root?: string): Appe
|
|
|
745
759
|
if (pr.commitsCount !== undefined) data.commitsCount = pr.commitsCount;
|
|
746
760
|
// draft/merged/mergeCommit are in the hash so a draft→ready or open→merged transition
|
|
747
761
|
// re-folds (a new event) rather than being deduped away as an unchanged observation.
|
|
748
|
-
const hash = evidenceHash([pr.number, pr.repository, pr.title, pr.body, pr.state, pr.draft, pr.merged, pr.mergeCommit, pr.baseRef, pr.headSha, pr.changedFiles, pr.commitsCount]);
|
|
762
|
+
const hash = evidenceHash([pr.number, pr.repository, pr.title, pr.body, pr.state, pr.draft, pr.merged, pr.mergeCommit, pr.baseRef, pr.headSha, pr.changedFiles, pr.commitsCount, pr.authorLogin, pr.authorType]);
|
|
749
763
|
const id = `github:pull_request:${pr.id}:${hash}`;
|
|
750
764
|
const event = {
|
|
751
765
|
id,
|
|
@@ -832,7 +846,7 @@ function foldObservedExistence(pr: ObservedPr, occurredAt: string, root?: string
|
|
|
832
846
|
});
|
|
833
847
|
}
|
|
834
848
|
});
|
|
835
|
-
(pr.comments ?? []).forEach((c, i) => emit('issue_comment', `c${i}`, {
|
|
849
|
+
(pr.comments ?? []).forEach((c, i) => emit('issue_comment', c.id === undefined ? `c${i}` : `ic:${c.id}`, {
|
|
836
850
|
...(c.body !== undefined ? { body: c.body } : {}),
|
|
837
851
|
...(c.createdAt !== undefined ? { created_at: c.createdAt } : {}),
|
|
838
852
|
}));
|
|
@@ -897,12 +911,13 @@ export async function syncGithubFromReal(
|
|
|
897
911
|
const prs = await pullGithubPrs(execute, { ...pullOpts, lastUpdatedAt: lastObservedPrUpdates(opts.root) });
|
|
898
912
|
const issues = await pullGithubIssues(execute, pullOpts);
|
|
899
913
|
// Generic shadow-diff fold (content-bearing resources: title/body/state + refs).
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
...(opts.root !== undefined ? { root: opts.root } : {}),
|
|
905
|
-
|
|
914
|
+
// PROTOCOL 2: the pack OBSERVES each resource; the kernel diffs it against the tree and folds what
|
|
915
|
+
// changed onto the root's log (observe.ts) — inside a refresh, collected and folded as a batch.
|
|
916
|
+
const pull = { observed: 0, deltasAppended: 0, unchanged: 0 };
|
|
917
|
+
for (const r of [...observedPrsToResources(prs), ...observedIssuesToResources(issues), ...(await observeRepositoryAndBranches(execute, opts.owner, opts.repo))]) {
|
|
918
|
+
const report = observeResource(SERVICE, { type: r.type, id: r.id, fields: r.fields }, { ...(opts.root !== undefined ? { root: opts.root } : {}), at: r.occurredAt ?? opts.occurredAt });
|
|
919
|
+
pull.observed += 1; pull.deltasAppended += report.appended; pull.unchanged += report.unchanged;
|
|
920
|
+
}
|
|
906
921
|
// GitHub fold (carries counts + review/comment content the delta drops, plus issues).
|
|
907
922
|
// A per-row `Conflicting duplicate` is tolerated (skipped + recorded), never fatal, so a
|
|
908
923
|
// single diverged row can't stop later resources in the same pull from folding.
|
|
@@ -999,8 +1014,12 @@ export async function pushGithubAction(
|
|
|
999
1014
|
// endpoint applyGithubWrite routes a PR edit through; GitHub accepts these on the issue
|
|
1000
1015
|
// PATCH. The PR PATCH itself ignores labels/assignees/milestone, so route those on the
|
|
1001
1016
|
// matching issue PATCH (PRs ARE issues for that endpoint) to faithfully persist them.
|
|
1002
|
-
|
|
1003
|
-
|
|
1017
|
+
let externalId = String(number);
|
|
1018
|
+
if (Object.keys(params).length > 3 || f.draft === undefined) {
|
|
1019
|
+
const res = await execute.request('PATCH /repos/{owner}/{repo}/pulls/{pull_number}', params);
|
|
1020
|
+
ensureOk(res, 'pulls.update');
|
|
1021
|
+
externalId = String(res.data?.number ?? number);
|
|
1022
|
+
}
|
|
1004
1023
|
if (f.labels !== undefined || f.assignees !== undefined || f.milestone !== undefined) {
|
|
1005
1024
|
const issueParams: Record<string, unknown> = { owner, repo, issue_number: number };
|
|
1006
1025
|
if (f.labels !== undefined) issueParams.labels = f.labels;
|
|
@@ -1009,7 +1028,24 @@ export async function pushGithubAction(
|
|
|
1009
1028
|
const issueRes = await execute.request('PATCH /repos/{owner}/{repo}/issues/{issue_number}', issueParams);
|
|
1010
1029
|
ensureOk(issueRes, 'pulls.update.issue_fields');
|
|
1011
1030
|
}
|
|
1012
|
-
|
|
1031
|
+
if (f.draft !== undefined) {
|
|
1032
|
+
if (typeof f.draft !== 'boolean') throw new Error('github push failed (pulls.draft): expected boolean');
|
|
1033
|
+
// Resolve the vendor's node, never send the local twin's synthetic GraphQL ID.
|
|
1034
|
+
const current = await execute.request('GET /repos/{owner}/{repo}/pulls/{pull_number}', { owner, repo, pull_number: number });
|
|
1035
|
+
ensureOk(current, 'pulls.draft.read');
|
|
1036
|
+
if (typeof current.data?.node_id !== 'string' || typeof current.data?.draft !== 'boolean') throw new Error('github push failed (pulls.draft): vendor PR identity/state missing');
|
|
1037
|
+
if (current.data.draft !== f.draft) {
|
|
1038
|
+
const mutation = f.draft ? 'convertPullRequestToDraft' : 'markPullRequestReadyForReview';
|
|
1039
|
+
const res = await execute.request('POST /graphql', {
|
|
1040
|
+
query: `mutation($id:ID!){${mutation}(input:{pullRequestId:$id}){pullRequest{id isDraft}}}`,
|
|
1041
|
+
variables: { id: current.data.node_id },
|
|
1042
|
+
});
|
|
1043
|
+
ensureOk(res, 'pulls.draft');
|
|
1044
|
+
const changed = res.data?.data?.[mutation]?.pullRequest;
|
|
1045
|
+
if (res.data?.errors?.length || changed?.id !== current.data.node_id || changed?.isDraft !== f.draft) throw new Error('github push failed (pulls.draft): GraphQL transition was not confirmed');
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
return { externalId };
|
|
1013
1049
|
}
|
|
1014
1050
|
if (op === 'pull_request.merge') {
|
|
1015
1051
|
const number = Number(f.number ?? action.subject.id.split('#').pop());
|
|
@@ -1220,57 +1256,6 @@ function knownReviewCommentExternalIds(root?: string): Record<string, string> {
|
|
|
1220
1256
|
return out;
|
|
1221
1257
|
}
|
|
1222
1258
|
|
|
1223
|
-
/**
|
|
1224
|
-
* PUSH (all): push every pending LOCAL GitHub action to real GitHub via the injected
|
|
1225
|
-
* executor and CONFIRM each on success — `confirmAction` records the confirmed fields
|
|
1226
|
-
* as an observed event and maps the action → that event, SUPPRESSING the local
|
|
1227
|
-
* projection (the fact now lives in the observed log; counted exactly once). Closes
|
|
1228
|
-
* the action → push → observed loop (R18). Idempotency: a confirmed action is no
|
|
1229
|
-
* longer pending, so a re-push calls the executor for it again ZERO times.
|
|
1230
|
-
*
|
|
1231
|
-
* PER-ACTION TOLERANCE, the same doctrine the pull's per-row conflict skip follows: one
|
|
1232
|
-
* action the connector cannot enact (a reply whose root has no id on the real repo) or that
|
|
1233
|
-
* the vendor rejected must NOT abort the other pending writes in the sweep — it is skipped
|
|
1234
|
-
* with its reason, left PENDING so a later sweep retries it, and REPORTED in `skipped` so a
|
|
1235
|
-
* poller can log it loudly instead of it being swallowed. Nothing is confirmed on a failure,
|
|
1236
|
-
* so a skipped action is never mistaken for a written one.
|
|
1237
|
-
*/
|
|
1238
|
-
export async function pushPendingGithubActions(
|
|
1239
|
-
execute: GithubExecute,
|
|
1240
|
-
opts: { root?: string; occurredAt: string },
|
|
1241
|
-
): Promise<{ pushed: number; confirmed: string[]; externalIds: Record<string, string>; skipped: Array<{ actionId: string; operation: string; reason: string }> }> {
|
|
1242
|
-
const confirmed: string[] = [];
|
|
1243
|
-
const externalIds: Record<string, string> = {};
|
|
1244
|
-
const skipped: Array<{ actionId: string; operation: string; reason: string }> = [];
|
|
1245
|
-
// Seeded from what the world already knows, then GROWN as the sweep confirms writes — a
|
|
1246
|
-
// root comment created and replied to between two pushes resolves within one sweep.
|
|
1247
|
-
const commentExternalIds = knownReviewCommentExternalIds(opts.root);
|
|
1248
|
-
for (const action of pendingActions(SERVICE, opts.root)) {
|
|
1249
|
-
let externalId: string;
|
|
1250
|
-
try {
|
|
1251
|
-
({ externalId } = await pushGithubAction(execute, action, { externalIds: commentExternalIds }));
|
|
1252
|
-
} catch (error) {
|
|
1253
|
-
skipped.push({ actionId: action.id, operation: action.operation ?? '', reason: error instanceof Error ? error.message : String(error) });
|
|
1254
|
-
continue;
|
|
1255
|
-
}
|
|
1256
|
-
confirmAction({
|
|
1257
|
-
service: SERVICE,
|
|
1258
|
-
actionId: action.id,
|
|
1259
|
-
subject: action.subject,
|
|
1260
|
-
fields: action.fields ?? {},
|
|
1261
|
-
occurredAt: opts.occurredAt,
|
|
1262
|
-
...(opts.root !== undefined ? { root: opts.root } : {}),
|
|
1263
|
-
});
|
|
1264
|
-
confirmed.push(action.id);
|
|
1265
|
-
externalIds[action.id] = externalId;
|
|
1266
|
-
// A review comment the vendor just accepted is now addressable, so a reply LATER in this
|
|
1267
|
-
// same sweep can name it. Keyed by the twin's own comment id, which is what the reply's
|
|
1268
|
-
// `in_reply_to` carries.
|
|
1269
|
-
const local = /^comment:(\d+)$/.exec(action.subject.id);
|
|
1270
|
-
if (local !== null && action.operation === 'pull_request_review_comment.create' && externalId !== '') commentExternalIds[local[1]!] = externalId;
|
|
1271
|
-
}
|
|
1272
|
-
return { pushed: confirmed.length, confirmed, externalIds, skipped };
|
|
1273
|
-
}
|
|
1274
1259
|
|
|
1275
1260
|
/**
|
|
1276
1261
|
* THE R14 PUSH ADAPTER for github (jira's `pushJiraToRemote` is the reference; this
|
|
@@ -1281,10 +1266,8 @@ export async function pushPendingGithubActions(
|
|
|
1281
1266
|
* loudly. The route grammar is the one `pushGithubAction` speaks (`METHOD /path/{param}`,
|
|
1282
1267
|
* templated params in the path, the rest as the JSON body).
|
|
1283
1268
|
*/
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
opts: { root?: string; origin?: string } = {},
|
|
1287
|
-
): Promise<{ pushed: number; confirmed: string[]; skipped: Array<{ actionId: string; operation: string; reason: string }> }> {
|
|
1269
|
+
/** This pack's vendor executor over the kernel's ONE RemoteExecute (push plane). */
|
|
1270
|
+
export function githubPushExecutor(execute: RemoteExecute): GithubExecute {
|
|
1288
1271
|
const executor: GithubExecute = {
|
|
1289
1272
|
async request(route, params = {}) {
|
|
1290
1273
|
const sp = route.indexOf(' ');
|
|
@@ -1316,12 +1299,40 @@ export async function pushGithubToRemote(
|
|
|
1316
1299
|
return { status: res.status, data: res.body === '' ? undefined : JSON.parse(res.body) };
|
|
1317
1300
|
},
|
|
1318
1301
|
};
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1302
|
+
return executor;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
/** THE ANCHORED PUSH SEAM (contract "The push arm is the kernel's transaction; a pack performs one
|
|
1306
|
+
* action"): perform ONE pending action against GitHub over the kernel executor; the vendor's id comes back. */
|
|
1307
|
+
/** The vendor's id in the pack's own subject grammar: a number replaces the trailing number of the
|
|
1308
|
+
* local id (`acme/web#issue:1` → `acme/web#issue:57`, `acme/web#3` → `acme/web#9`, `comment:2` →
|
|
1309
|
+
* `comment:184`); anything else (a sha, a ref, a path, an owner/name) leaves the address as it is
|
|
1310
|
+
* and rides on the receipt as `vendorId`. The kernel rebinds the subject to what comes back
|
|
1311
|
+
* (contract "A pushed write adopts the vendor's id"). */
|
|
1312
|
+
export function adoptGithubId(localId: string, vendorId: string | undefined): string {
|
|
1313
|
+
if (!vendorId) return localId;
|
|
1314
|
+
if (/^\d+$/.test(vendorId) && /\d+$/.test(localId)) return localId.replace(/\d+$/, vendorId);
|
|
1315
|
+
if (vendorId === localId || vendorId.includes('#') || vendorId.startsWith('comment:') || vendorId.startsWith('review:')) return vendorId;
|
|
1316
|
+
return localId;
|
|
1323
1317
|
}
|
|
1324
1318
|
|
|
1319
|
+
export async function performGithubAction(execute: RemoteExecute, action: TwinAction, ctx: PerformContext): Promise<PushOutcome> {
|
|
1320
|
+
const externalIds = knownReviewCommentExternalIds(ctx.root);
|
|
1321
|
+
const replyTo = action.fields?.in_reply_to;
|
|
1322
|
+
if (replyTo !== undefined) {
|
|
1323
|
+
const local = `comment:${replyTo}`;
|
|
1324
|
+
const adopted = ctx.resolve('pull_request_review_comment', local);
|
|
1325
|
+
if (adopted !== local && /^comment:\d+$/.test(adopted)) externalIds[String(replyTo)] = adopted.slice('comment:'.length);
|
|
1326
|
+
}
|
|
1327
|
+
const out = await pushGithubAction(githubPushExecutor(execute), action, { externalIds });
|
|
1328
|
+
const adopted = adoptGithubId(action.subject.id, out.externalId);
|
|
1329
|
+
// what the vendor minted rides on the receipt as fields: the number under which the row now lives
|
|
1330
|
+
const numeric = out.externalId !== undefined && /^\d+$/.test(out.externalId) && adopted !== action.subject.id;
|
|
1331
|
+
const data = { ...(numeric ? { number: Number(out.externalId) } : {}), ...(out.externalId && out.externalId !== adopted ? { vendorId: out.externalId } : {}) };
|
|
1332
|
+
return { externalId: adopted, ...(Object.keys(data).length ? { data } : {}) };
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
|
|
1325
1336
|
/**
|
|
1326
1337
|
* THE REPOSITORY AND ITS BRANCHES are observed with the PRs and issues: a working copy's
|
|
1327
1338
|
* confirmed writes (a branch it cut, the repo it declared) survive only as what reality
|
|
@@ -1369,7 +1380,8 @@ export async function syncGithubFromRemote(
|
|
|
1369
1380
|
execute: RemoteExecute,
|
|
1370
1381
|
opts: { root?: string; origin?: string; state?: 'open' | 'closed' | 'all'; perPage?: number } = {},
|
|
1371
1382
|
): Promise<{ observed: number; deltasAppended: number; eventsAppended: number; issues: number; conflictsSkipped: number; conflictingIds: string[] }> {
|
|
1372
|
-
|
|
1383
|
+
// the repository is the end of the path: a served world's twin lives under /<org>/<world>/github
|
|
1384
|
+
const match = /\/repos\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/.exec(opts.origin === undefined ? '' : new URL(opts.origin).pathname);
|
|
1373
1385
|
if (match === null) {
|
|
1374
1386
|
throw new Error('github remote pull: the link origin must name the repo — https://api.github.com/repos/{owner}/{repo}');
|
|
1375
1387
|
}
|