@grimoire-rs/indexer 0.3.3 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +220 -9
- package/dist/ci.d.ts +6 -1
- package/dist/ci.d.ts.map +1 -1
- package/dist/ci.js +32 -1
- package/dist/ci.js.map +1 -1
- package/dist/cli/build.d.ts.map +1 -1
- package/dist/cli/build.js +3 -1
- package/dist/cli/build.js.map +1 -1
- package/dist/cli/ci.d.ts.map +1 -1
- package/dist/cli/ci.js +5 -1
- package/dist/cli/ci.js.map +1 -1
- package/dist/cli/enrich.d.ts +1 -0
- package/dist/cli/enrich.d.ts.map +1 -1
- package/dist/cli/enrich.js +38 -0
- package/dist/cli/enrich.js.map +1 -1
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +17 -3
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +9 -0
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/ratings.d.ts +3 -0
- package/dist/cli/ratings.d.ts.map +1 -0
- package/dist/cli/ratings.js +160 -0
- package/dist/cli/ratings.js.map +1 -0
- package/dist/config.d.ts +19 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +47 -1
- package/dist/config.js.map +1 -1
- package/dist/data/index.d.ts.map +1 -1
- package/dist/data/index.js +13 -0
- package/dist/data/index.js.map +1 -1
- package/dist/enrich/checkpoint.d.ts +53 -0
- package/dist/enrich/checkpoint.d.ts.map +1 -0
- package/dist/enrich/checkpoint.js +296 -0
- package/dist/enrich/checkpoint.js.map +1 -0
- package/dist/enrich/index.d.ts +14 -0
- package/dist/enrich/index.d.ts.map +1 -1
- package/dist/enrich/index.js +77 -4
- package/dist/enrich/index.js.map +1 -1
- package/dist/ratings/budget.d.ts +16 -0
- package/dist/ratings/budget.d.ts.map +1 -0
- package/dist/ratings/budget.js +7 -0
- package/dist/ratings/budget.js.map +1 -0
- package/dist/ratings/config.d.ts +56 -0
- package/dist/ratings/config.d.ts.map +1 -0
- package/dist/ratings/config.js +86 -0
- package/dist/ratings/config.js.map +1 -0
- package/dist/ratings/marker.d.ts +105 -0
- package/dist/ratings/marker.d.ts.map +1 -0
- package/dist/ratings/marker.js +141 -0
- package/dist/ratings/marker.js.map +1 -0
- package/dist/ratings/paging.d.ts +16 -0
- package/dist/ratings/paging.d.ts.map +1 -0
- package/dist/ratings/paging.js +36 -0
- package/dist/ratings/paging.js.map +1 -0
- package/dist/ratings/provider.d.ts +100 -0
- package/dist/ratings/provider.d.ts.map +1 -0
- package/dist/ratings/provider.js +203 -0
- package/dist/ratings/provider.js.map +1 -0
- package/dist/ratings/provider_github.d.ts +3 -0
- package/dist/ratings/provider_github.d.ts.map +1 -0
- package/dist/ratings/provider_github.js +135 -0
- package/dist/ratings/provider_github.js.map +1 -0
- package/dist/ratings/provider_gitlab.d.ts +3 -0
- package/dist/ratings/provider_gitlab.d.ts.map +1 -0
- package/dist/ratings/provider_gitlab.js +153 -0
- package/dist/ratings/provider_gitlab.js.map +1 -0
- package/dist/ratings/provider_memory.d.ts +28 -0
- package/dist/ratings/provider_memory.d.ts.map +1 -0
- package/dist/ratings/provider_memory.js +49 -0
- package/dist/ratings/provider_memory.js.map +1 -0
- package/dist/ratings/reconcile.d.ts +47 -0
- package/dist/ratings/reconcile.d.ts.map +1 -0
- package/dist/ratings/reconcile.js +133 -0
- package/dist/ratings/reconcile.js.map +1 -0
- package/dist/ratings/seed.d.ts +60 -0
- package/dist/ratings/seed.d.ts.map +1 -0
- package/dist/ratings/seed.js +132 -0
- package/dist/ratings/seed.js.map +1 -0
- package/dist/renderer/astro/components/Catalog.d.ts +2 -0
- package/dist/renderer/astro/components/Catalog.js +65 -16
- package/dist/renderer/astro/components/Catalog.tsx +119 -26
- package/dist/renderer/astro/components/CommandField.astro +1 -0
- package/dist/renderer/astro/components/VersionMenu.astro +31 -23
- package/dist/renderer/astro/layouts/Base.astro +218 -263
- package/dist/renderer/astro/lib/catalog.d.ts +25 -0
- package/dist/renderer/astro/lib/catalog.js +46 -0
- package/dist/renderer/astro/lib/catalog.ts +49 -0
- package/dist/renderer/astro/pages/p/[...slug].astro +238 -85
- package/dist/renderer/astro/styles/tokens.css +157 -0
- package/dist/renderer/index.d.ts.map +1 -1
- package/dist/renderer/index.js +101 -2
- package/dist/renderer/index.js.map +1 -1
- package/dist/renderer/types.d.ts +41 -0
- package/dist/renderer/types.d.ts.map +1 -1
- package/dist/validate/adapters/http.d.ts +29 -1
- package/dist/validate/adapters/http.d.ts.map +1 -1
- package/dist/validate/adapters/http.js +17 -2
- package/dist/validate/adapters/http.js.map +1 -1
- package/package.json +1 -1
- package/templates/ci/github-enrich.yml +9 -1
- package/templates/ci/github-pages.yml +11 -5
- package/templates/ci/github-ratings-seed.yml +88 -0
- package/templates/ci/github-ratings.yml +51 -0
- package/templates/ci/gitlab-ci.yml +3 -3
- package/templates/ci/gitlab-enrich.sh +10 -1
- package/templates/ci/gitlab-ratings-seed.sh +83 -0
- package/templates/ci/gitlab-ratings.yml +37 -0
- package/templates/gitignore +8 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright 2026 The Grimoire Authors
|
|
3
|
+
/**
|
|
4
|
+
* The `RatingProvider` seam — one interface, two forges, one in-memory fake.
|
|
5
|
+
*
|
|
6
|
+
* Mirrors `validate/adapters/forge.ts`: an interface, per-forge closures, and a
|
|
7
|
+
* `create*` factory that picks between them. The GraphQL plumbing every
|
|
8
|
+
* provider needs lives here rather than in each of them, because the one rule
|
|
9
|
+
* that must not drift is [`graphql`]'s error branching — a 200 carrying a
|
|
10
|
+
* populated `errors` array is a hard error, and two copies of that check is how
|
|
11
|
+
* one of them turns into an empty tally.
|
|
12
|
+
*/
|
|
13
|
+
import { CliError, EXIT } from "../cli/exit.js";
|
|
14
|
+
import { LARGE_RESPONSE_BYTES, request } from "../validate/adapters/http.js";
|
|
15
|
+
import { buildMarker } from "./marker.js";
|
|
16
|
+
// The factory owns the dispatch, so it imports both implementations while they
|
|
17
|
+
// import the shared plumbing back. ESM handles the cycle: everything crossing it
|
|
18
|
+
// is a hoisted function declaration, referenced only from inside a closure.
|
|
19
|
+
//
|
|
20
|
+
// A `const` read at module scope is the case that rule excludes, and `PAGE_SIZE`
|
|
21
|
+
// was one -- see `paging.ts`, which is why it lives there and not here. Re-exported
|
|
22
|
+
// so callers still have one place to look.
|
|
23
|
+
export { PAGE_SIZE } from "./paging.js";
|
|
24
|
+
import { githubProvider as github } from "./provider_github.js";
|
|
25
|
+
import { gitlabProvider as gitlab } from "./provider_gitlab.js";
|
|
26
|
+
/**
|
|
27
|
+
* The forge answered, and the answer was unusable: a transport failure, a
|
|
28
|
+
* non-2xx, a body that did not parse, or a 200 carrying GraphQL `errors`.
|
|
29
|
+
*
|
|
30
|
+
* A `CliError` subclass rather than a name in `classify()`'s list, following
|
|
31
|
+
* `seed.ts`: that list exists for errors thrown by modules `main.ts` must not
|
|
32
|
+
* import, and `cli/exit.js` is already one of its static imports — so the
|
|
33
|
+
* mapping here is by type, which cannot drift out of sync with a string.
|
|
34
|
+
*/
|
|
35
|
+
export class ForgeError extends CliError {
|
|
36
|
+
constructor(message, code = EXIT.unavailable) {
|
|
37
|
+
super(message, code);
|
|
38
|
+
this.name = "RatingsForgeError";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* A secondary rate limit. Distinguished from [`ForgeError`] because the two are
|
|
43
|
+
* handled oppositely: a forge error fails the run, a rate limit stops it early
|
|
44
|
+
* and publishes what it already got.
|
|
45
|
+
*/
|
|
46
|
+
export class RateLimited extends ForgeError {
|
|
47
|
+
/** What the forge asked us to wait, by GitHub's documented precedence. */
|
|
48
|
+
retryAfterMs;
|
|
49
|
+
/** Threads read before the limit. Empty on the create path. */
|
|
50
|
+
observed;
|
|
51
|
+
constructor(message, retryAfterMs, observed = []) {
|
|
52
|
+
super(message);
|
|
53
|
+
this.name = "RatingsRateLimited";
|
|
54
|
+
this.retryAfterMs = retryAfterMs;
|
|
55
|
+
this.observed = observed;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** GitHub's documented floor when neither header says how long to wait. */
|
|
59
|
+
export const BACKOFF_FLOOR_MS = 60_000;
|
|
60
|
+
/**
|
|
61
|
+
* The body every provider writes. The marker sits on its own line, which is the
|
|
62
|
+
* only position R-1's anchored parser accepts; the prose above it is for the
|
|
63
|
+
* human who lands on the thread from the catalog.
|
|
64
|
+
*/
|
|
65
|
+
export function threadBody(ref) {
|
|
66
|
+
return [
|
|
67
|
+
`Rating thread for \`${ref}\`.`,
|
|
68
|
+
"",
|
|
69
|
+
"Upvote this thread to rate the package. Replies are not read by the tally.",
|
|
70
|
+
"",
|
|
71
|
+
buildMarker(ref),
|
|
72
|
+
].join("\n");
|
|
73
|
+
}
|
|
74
|
+
export function field(data, key) {
|
|
75
|
+
if (typeof data !== "object" || data === null)
|
|
76
|
+
return undefined;
|
|
77
|
+
return data[key];
|
|
78
|
+
}
|
|
79
|
+
/** `field`, chained. Every GraphQL read here is a path into an `unknown`. */
|
|
80
|
+
export function at(data, ...keys) {
|
|
81
|
+
return keys.reduce((value, key) => field(value, key), data);
|
|
82
|
+
}
|
|
83
|
+
export function stringAt(data, ...keys) {
|
|
84
|
+
const value = at(data, ...keys);
|
|
85
|
+
return typeof value === "string" ? value : "";
|
|
86
|
+
}
|
|
87
|
+
export function nodes(data) {
|
|
88
|
+
const value = at(data, "nodes");
|
|
89
|
+
return Array.isArray(value) ? value : [];
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* How long the forge asked us to wait: `retry-after` first, then
|
|
93
|
+
* `x-ratelimit-reset`, else the documented floor. GitHub's guidance is explicit
|
|
94
|
+
* that continuing to make requests while rate limited can get an integration
|
|
95
|
+
* banned, so this value is honoured by *stopping*, which is the strongest form
|
|
96
|
+
* of honouring it — a scheduled run resumes on the next tick, and sleeping here
|
|
97
|
+
* would burn CI minutes waiting to make requests the design has already decided
|
|
98
|
+
* not to make.
|
|
99
|
+
*/
|
|
100
|
+
function retryAfterMs(response) {
|
|
101
|
+
const after = Number(response.header("retry-after"));
|
|
102
|
+
if (Number.isFinite(after) && after > 0)
|
|
103
|
+
return after * 1000;
|
|
104
|
+
const reset = Number(response.header("x-ratelimit-reset"));
|
|
105
|
+
if (Number.isFinite(reset) && reset > 0) {
|
|
106
|
+
const wait = reset * 1000 - Date.now();
|
|
107
|
+
if (wait > 0)
|
|
108
|
+
return wait;
|
|
109
|
+
}
|
|
110
|
+
return BACKOFF_FLOOR_MS;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Is this reply a rate limit rather than an ordinary refusal?
|
|
114
|
+
*
|
|
115
|
+
* 429 always. 403 only when it says so — GitHub returns 403 for both a
|
|
116
|
+
* secondary limit and a plain permission failure, and the two are handled
|
|
117
|
+
* oppositely, so the discrimination has to be explicit.
|
|
118
|
+
*/
|
|
119
|
+
function rateLimitOf(response) {
|
|
120
|
+
if (response.status === 429)
|
|
121
|
+
return retryAfterMs(response);
|
|
122
|
+
if (response.status !== 403)
|
|
123
|
+
return null;
|
|
124
|
+
const secondary = /secondary rate limit|rate limit/i.test(response.body) ||
|
|
125
|
+
response.header("retry-after") !== "" ||
|
|
126
|
+
response.header("x-ratelimit-remaining") === "0";
|
|
127
|
+
return secondary ? retryAfterMs(response) : null;
|
|
128
|
+
}
|
|
129
|
+
function graphqlErrors(parsed) {
|
|
130
|
+
const errors = field(parsed, "errors");
|
|
131
|
+
if (!Array.isArray(errors) || errors.length === 0)
|
|
132
|
+
return null;
|
|
133
|
+
const message = errors
|
|
134
|
+
.map((err) => stringAt(err, "message") || JSON.stringify(err))
|
|
135
|
+
.join("; ");
|
|
136
|
+
const limited = errors.some((err) => stringAt(err, "type") === "RATE_LIMITED" ||
|
|
137
|
+
stringAt(err, "extensions", "code") === "RATE_LIMITED");
|
|
138
|
+
return { message, limited };
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* One GraphQL round trip, with the branching every provider shares.
|
|
142
|
+
*
|
|
143
|
+
* Order matters and is the reason this is one function: a rate limit is
|
|
144
|
+
* classified first (it is the one failure that must not fail the run), then
|
|
145
|
+
* `errors` is checked **independently of the HTTP status and before `data` is
|
|
146
|
+
* touched** — a 200 with `errors` and a null `data` otherwise reads as
|
|
147
|
+
* "genuinely fewer votes observed", which is silent emptying arriving through
|
|
148
|
+
* the one door R-2 does not watch.
|
|
149
|
+
*
|
|
150
|
+
* @param observed Threads already read, attached to a [`RateLimited`] so a
|
|
151
|
+
* truncated pagination pass stays partial rather than becoming empty.
|
|
152
|
+
*/
|
|
153
|
+
export async function graphql(api, headers, query, variables, observed = []) {
|
|
154
|
+
const response = await request(api, { ...headers, "Content-Type": "application/json" }, {
|
|
155
|
+
method: "POST",
|
|
156
|
+
body: JSON.stringify({ query, variables }),
|
|
157
|
+
// A page is `PAGE_SIZE` threads and every one of their bodies. The default
|
|
158
|
+
// 1 MiB is sized for a small JSON document, and overrunning it lands as
|
|
159
|
+
// `status: 0` below — reported as a transport failure, which is what an
|
|
160
|
+
// oversized first page was being misdiagnosed as.
|
|
161
|
+
maxBytes: LARGE_RESPONSE_BYTES,
|
|
162
|
+
});
|
|
163
|
+
const wait = rateLimitOf(response);
|
|
164
|
+
if (wait !== null) {
|
|
165
|
+
throw new RateLimited(`${api}: secondary rate limit (HTTP ${response.status}), retry after ${Math.round(wait / 1000)}s`, wait, observed);
|
|
166
|
+
}
|
|
167
|
+
let parsed;
|
|
168
|
+
let parseError = null;
|
|
169
|
+
try {
|
|
170
|
+
parsed = JSON.parse(response.body);
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
parseError = err.message;
|
|
174
|
+
}
|
|
175
|
+
const errors = graphqlErrors(parsed);
|
|
176
|
+
if (errors) {
|
|
177
|
+
if (errors.limited)
|
|
178
|
+
throw new RateLimited(`${api}: ${errors.message}`, BACKOFF_FLOOR_MS, observed);
|
|
179
|
+
throw new ForgeError(`${api}: ${errors.message}`);
|
|
180
|
+
}
|
|
181
|
+
// `status: 0` is `request()`'s transport catch *and* its response-size cap.
|
|
182
|
+
// Neither says anything about what the forge holds, so both are hard errors.
|
|
183
|
+
if (response.status === 0) {
|
|
184
|
+
throw new ForgeError(`${api}: the request did not complete (transport failure or oversized reply)`);
|
|
185
|
+
}
|
|
186
|
+
if (response.status < 200 || response.status >= 300) {
|
|
187
|
+
throw new ForgeError(`${api}: HTTP ${response.status}`);
|
|
188
|
+
}
|
|
189
|
+
if (parseError !== null) {
|
|
190
|
+
throw new ForgeError(`${api}: the reply did not parse (${parseError})`);
|
|
191
|
+
}
|
|
192
|
+
const data = field(parsed, "data");
|
|
193
|
+
if (typeof data !== "object" || data === null) {
|
|
194
|
+
throw new ForgeError(`${api}: the reply carried no data`);
|
|
195
|
+
}
|
|
196
|
+
return data;
|
|
197
|
+
}
|
|
198
|
+
export function createRatingProvider(kind, config) {
|
|
199
|
+
// Deliberately a `?:` and not a registry: two forges, one dispatch site, the
|
|
200
|
+
// same shape `createForge` already uses.
|
|
201
|
+
return kind === "github" ? github(config) : gitlab(config);
|
|
202
|
+
}
|
|
203
|
+
//# sourceMappingURL=provider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider.js","sourceRoot":"","sources":["../../src/ratings/provider.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider_github.d.ts","sourceRoot":"","sources":["../../src/ratings/provider_github.ts"],"names":[],"mappings":"AAYA,OAAO,EAOL,KAAK,cAAc,EACnB,KAAK,oBAAoB,EAE1B,MAAM,eAAe,CAAC;AA0CvB,wBAAgB,cAAc,CAAC,MAAM,EAAE,oBAAoB,GAAG,cAAc,CAiG3E"}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright 2026 The Grimoire Authors
|
|
3
|
+
/**
|
|
4
|
+
* GitHub Discussions — GraphQL only, no REST equivalent.
|
|
5
|
+
*
|
|
6
|
+
* Upvotes are first-class here: `upvoteCount` is a scalar on the discussion, so
|
|
7
|
+
* the tally costs ~1 GraphQL point per page. Counting `reactionGroups` nodes
|
|
8
|
+
* instead would multiply nested-node cost for the same number.
|
|
9
|
+
*/
|
|
10
|
+
import { authorizeThread } from "./marker.js";
|
|
11
|
+
import { ForgeError, at, graphql, nodes, threadBody, stringAt, } from "./provider.js";
|
|
12
|
+
import { PAGE_SIZE } from "./paging.js";
|
|
13
|
+
import { EXIT } from "../cli/exit.js";
|
|
14
|
+
const CATEGORIES = `
|
|
15
|
+
query($owner:String!,$name:String!){
|
|
16
|
+
repository(owner:$owner,name:$name){
|
|
17
|
+
id
|
|
18
|
+
discussionCategories(first:100){nodes{id name}}
|
|
19
|
+
}
|
|
20
|
+
}`;
|
|
21
|
+
// `databaseId` rather than the node `id`: R-1 clause 2 compares against
|
|
22
|
+
// `index-policy.json`'s `trustedBots[].id`, which is the numeric account id the
|
|
23
|
+
// validate gate already writes there — the opaque node id would never match.
|
|
24
|
+
const DISCUSSIONS = `
|
|
25
|
+
query($owner:String!,$name:String!,$categoryId:ID!,$cursor:String){
|
|
26
|
+
repository(owner:$owner,name:$name){
|
|
27
|
+
discussions(first:${PAGE_SIZE},after:$cursor,categoryId:$categoryId){
|
|
28
|
+
pageInfo{hasNextPage endCursor}
|
|
29
|
+
nodes{
|
|
30
|
+
id url upvoteCount body
|
|
31
|
+
category{id}
|
|
32
|
+
repository{id}
|
|
33
|
+
author{...on User{databaseId} ...on Bot{databaseId}}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}`;
|
|
38
|
+
const CREATE = `
|
|
39
|
+
mutation($repositoryId:ID!,$categoryId:ID!,$title:String!,$body:String!){
|
|
40
|
+
createDiscussion(input:{repositoryId:$repositoryId,categoryId:$categoryId,title:$title,body:$body}){
|
|
41
|
+
discussion{id url upvoteCount}
|
|
42
|
+
}
|
|
43
|
+
}`;
|
|
44
|
+
const LOCK = `
|
|
45
|
+
mutation($lockableId:ID!){
|
|
46
|
+
lockLockable(input:{lockableId:$lockableId}){clientMutationId}
|
|
47
|
+
}`;
|
|
48
|
+
export function githubProvider(config) {
|
|
49
|
+
const headers = { Authorization: `Bearer ${config.token}` };
|
|
50
|
+
const [owner = "", name = ""] = config.project.split("/", 2);
|
|
51
|
+
let resolved = null;
|
|
52
|
+
/** `container` is a name the operator typed; R-1 compares ids, so resolve it once. */
|
|
53
|
+
async function resolve() {
|
|
54
|
+
if (resolved)
|
|
55
|
+
return resolved;
|
|
56
|
+
const data = await graphql(config.api, headers, CATEGORIES, { owner, name });
|
|
57
|
+
const repositoryId = stringAt(data, "repository", "id");
|
|
58
|
+
if (repositoryId === "") {
|
|
59
|
+
throw new ForgeError(`${config.project}: no such repository, or it is not visible to this token`);
|
|
60
|
+
}
|
|
61
|
+
const wanted = config.container.trim().toLowerCase();
|
|
62
|
+
const categories = nodes(at(data, "repository", "discussionCategories"));
|
|
63
|
+
const match = categories.find((node) => stringAt(node, "name").trim().toLowerCase() === wanted);
|
|
64
|
+
if (!match) {
|
|
65
|
+
throw new ForgeError(`${config.project}: no discussion category named ${JSON.stringify(config.container)} ` +
|
|
66
|
+
`(found: ${categories.map((node) => stringAt(node, "name")).join(", ") || "none"})`,
|
|
67
|
+
// The operator's `container` is wrong, not the forge — a data error, so
|
|
68
|
+
// a retry on the next schedule cannot fix it and should not pretend to.
|
|
69
|
+
EXIT.data);
|
|
70
|
+
}
|
|
71
|
+
resolved = { repositoryId, categoryId: stringAt(match, "id") };
|
|
72
|
+
return resolved;
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
async listAuthored() {
|
|
76
|
+
const { repositoryId, categoryId } = await resolve();
|
|
77
|
+
const threads = [];
|
|
78
|
+
let cursor = null;
|
|
79
|
+
for (;;) {
|
|
80
|
+
const data = await graphql(config.api, headers, DISCUSSIONS, { owner, name, categoryId, cursor }, threads);
|
|
81
|
+
const page = at(data, "repository", "discussions");
|
|
82
|
+
for (const node of nodes(page)) {
|
|
83
|
+
const author = at(node, "author");
|
|
84
|
+
const databaseId = at(author, "databaseId");
|
|
85
|
+
const observed = {
|
|
86
|
+
target: stringAt(node, "id"),
|
|
87
|
+
url: stringAt(node, "url"),
|
|
88
|
+
up: typeof at(node, "upvoteCount") === "number" ? at(node, "upvoteCount") : 0,
|
|
89
|
+
body: stringAt(node, "body"),
|
|
90
|
+
authorId: typeof databaseId === "number" ? String(databaseId) : undefined,
|
|
91
|
+
containerId: stringAt(node, "repository", "id"),
|
|
92
|
+
categoryId: stringAt(node, "category", "id"),
|
|
93
|
+
};
|
|
94
|
+
const authorized = authorizeThread(observed, {
|
|
95
|
+
trustedBots: config.trustedBots,
|
|
96
|
+
containerId: repositoryId,
|
|
97
|
+
categoryId,
|
|
98
|
+
});
|
|
99
|
+
if (authorized)
|
|
100
|
+
threads.push(authorized);
|
|
101
|
+
}
|
|
102
|
+
if (at(page, "pageInfo", "hasNextPage") !== true)
|
|
103
|
+
return threads;
|
|
104
|
+
cursor = stringAt(page, "pageInfo", "endCursor");
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
async create(ref) {
|
|
108
|
+
const { repositoryId, categoryId } = await resolve();
|
|
109
|
+
const data = await graphql(config.api, headers, CREATE, {
|
|
110
|
+
repositoryId,
|
|
111
|
+
categoryId,
|
|
112
|
+
title: ref,
|
|
113
|
+
body: threadBody(ref),
|
|
114
|
+
});
|
|
115
|
+
const discussion = at(data, "createDiscussion", "discussion");
|
|
116
|
+
const target = stringAt(discussion, "id");
|
|
117
|
+
if (target === "")
|
|
118
|
+
throw new ForgeError(`${config.project}: createDiscussion returned no discussion`);
|
|
119
|
+
// Locking is the promise `lockThreads: true` makes — votes count, replies
|
|
120
|
+
// are refused — so a failure here fails the run rather than silently
|
|
121
|
+
// leaving an open forum the operator did not ask to moderate. Turn it off
|
|
122
|
+
// in `index.config.json` if the forge cannot honour it.
|
|
123
|
+
if (config.lockThreads) {
|
|
124
|
+
await graphql(config.api, headers, LOCK, { lockableId: target });
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
ref,
|
|
128
|
+
target,
|
|
129
|
+
url: stringAt(discussion, "url"),
|
|
130
|
+
up: typeof at(discussion, "upvoteCount") === "number" ? at(discussion, "upvoteCount") : 0,
|
|
131
|
+
};
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=provider_github.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider_github.js","sourceRoot":"","sources":["../../src/ratings/provider_github.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider_gitlab.d.ts","sourceRoot":"","sources":["../../src/ratings/provider_gitlab.ts"],"names":[],"mappings":"AAcA,OAAO,EAOL,KAAK,cAAc,EACnB,KAAK,oBAAoB,EAE1B,MAAM,eAAe,CAAC;AAgEvB,wBAAgB,cAAc,CAAC,MAAM,EAAE,oBAAoB,GAAG,cAAc,CA8F3E"}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright 2026 The Grimoire Authors
|
|
3
|
+
/**
|
|
4
|
+
* GitLab work items — the GraphQL surface, not the long-term-deprecated REST
|
|
5
|
+
* issues API.
|
|
6
|
+
*
|
|
7
|
+
* The counter lives on the **Emoji Reactions** widget (`upvotes`). GitLab
|
|
8
|
+
* renamed the feature from Award Emoji in 16.0 while leaving the GraphQL names
|
|
9
|
+
* (`AwardEmoji`, `awardEmojiToggle`) unchanged, so the schema below reads
|
|
10
|
+
* legacy and the prose reads current — that is the rename, not a mismatch.
|
|
11
|
+
*/
|
|
12
|
+
import { authorizeThread } from "./marker.js";
|
|
13
|
+
import { ForgeError, at, graphql, nodes, threadBody, stringAt, } from "./provider.js";
|
|
14
|
+
import { PAGE_SIZE } from "./paging.js";
|
|
15
|
+
import { EXIT } from "../cli/exit.js";
|
|
16
|
+
const TYPES = `
|
|
17
|
+
query($path:ID!){
|
|
18
|
+
project(fullPath:$path){
|
|
19
|
+
id
|
|
20
|
+
workItemTypes{nodes{id name}}
|
|
21
|
+
}
|
|
22
|
+
}`;
|
|
23
|
+
const WORK_ITEMS = `
|
|
24
|
+
query($path:ID!,$types:[IssueType!],$cursor:String){
|
|
25
|
+
project(fullPath:$path){
|
|
26
|
+
id
|
|
27
|
+
workItems(first:${PAGE_SIZE},after:$cursor,types:$types){
|
|
28
|
+
pageInfo{hasNextPage endCursor}
|
|
29
|
+
nodes{
|
|
30
|
+
id webUrl
|
|
31
|
+
author{id}
|
|
32
|
+
workItemType{id}
|
|
33
|
+
widgets{
|
|
34
|
+
...on WorkItemWidgetDescription{description}
|
|
35
|
+
...on WorkItemWidgetAwardEmoji{upvotes}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}`;
|
|
41
|
+
const CREATE = `
|
|
42
|
+
mutation($path:ID!,$typeId:WorkItemsTypeID!,$title:String!,$body:String!){
|
|
43
|
+
workItemCreate(input:{namespacePath:$path,workItemTypeId:$typeId,title:$title,descriptionWidget:{description:$body}}){
|
|
44
|
+
workItem{id webUrl}
|
|
45
|
+
}
|
|
46
|
+
}`;
|
|
47
|
+
const LOCK = `
|
|
48
|
+
mutation($id:WorkItemID!){
|
|
49
|
+
workItemUpdate(input:{id:$id,notesWidget:{discussionLocked:true}}){
|
|
50
|
+
workItem{id}
|
|
51
|
+
}
|
|
52
|
+
}`;
|
|
53
|
+
/**
|
|
54
|
+
* The numeric tail of a global id — `gid://gitlab/User/42` → `42`.
|
|
55
|
+
*
|
|
56
|
+
* Only the **author** id is normalized, and only because R-1 clause 2 compares
|
|
57
|
+
* it against `index-policy.json`'s `trustedBots[].id`, which the validate gate
|
|
58
|
+
* writes as GitLab's numeric user id. Container and type ids stay whole gids:
|
|
59
|
+
* both sides of those comparisons come from this same provider, so there is
|
|
60
|
+
* nothing to agree with.
|
|
61
|
+
*/
|
|
62
|
+
function numericId(gid) {
|
|
63
|
+
const tail = gid.split("/").pop();
|
|
64
|
+
return tail !== undefined && /^\d+$/.test(tail) ? tail : undefined;
|
|
65
|
+
}
|
|
66
|
+
/** `Issue` → `ISSUE`, `Test case` → `TEST_CASE`. The `types:` filter's enum. */
|
|
67
|
+
function typeEnum(container) {
|
|
68
|
+
return container.trim().toUpperCase().replace(/\s+/g, "_");
|
|
69
|
+
}
|
|
70
|
+
export function gitlabProvider(config) {
|
|
71
|
+
const headers = { Authorization: `Bearer ${config.token}` };
|
|
72
|
+
const path = config.project;
|
|
73
|
+
let resolved = null;
|
|
74
|
+
async function resolve() {
|
|
75
|
+
if (resolved)
|
|
76
|
+
return resolved;
|
|
77
|
+
const data = await graphql(config.api, headers, TYPES, { path });
|
|
78
|
+
const projectId = stringAt(data, "project", "id");
|
|
79
|
+
if (projectId === "") {
|
|
80
|
+
throw new ForgeError(`${path}: no such project, or it is not visible to this token`);
|
|
81
|
+
}
|
|
82
|
+
const wanted = config.container.trim().toLowerCase();
|
|
83
|
+
const types = nodes(at(data, "project", "workItemTypes"));
|
|
84
|
+
const match = types.find((node) => stringAt(node, "name").trim().toLowerCase() === wanted);
|
|
85
|
+
if (!match) {
|
|
86
|
+
throw new ForgeError(`${path}: no work item type named ${JSON.stringify(config.container)} ` +
|
|
87
|
+
`(found: ${types.map((node) => stringAt(node, "name")).join(", ") || "none"})`, EXIT.data);
|
|
88
|
+
}
|
|
89
|
+
resolved = { projectId, typeId: stringAt(match, "id") };
|
|
90
|
+
return resolved;
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
async listAuthored() {
|
|
94
|
+
const { projectId, typeId } = await resolve();
|
|
95
|
+
const threads = [];
|
|
96
|
+
let cursor = null;
|
|
97
|
+
for (;;) {
|
|
98
|
+
const data = await graphql(config.api, headers, WORK_ITEMS, { path, types: [typeEnum(config.container)], cursor }, threads);
|
|
99
|
+
const page = at(data, "project", "workItems");
|
|
100
|
+
for (const node of nodes(page)) {
|
|
101
|
+
const widgets = at(node, "widgets");
|
|
102
|
+
const list = Array.isArray(widgets) ? widgets : [];
|
|
103
|
+
const upvotes = list.map((w) => at(w, "upvotes")).find((v) => typeof v === "number");
|
|
104
|
+
const description = list.map((w) => at(w, "description")).find((v) => typeof v === "string");
|
|
105
|
+
const observed = {
|
|
106
|
+
target: stringAt(node, "id"),
|
|
107
|
+
url: stringAt(node, "webUrl"),
|
|
108
|
+
up: typeof upvotes === "number" ? upvotes : 0,
|
|
109
|
+
body: typeof description === "string" ? description : "",
|
|
110
|
+
authorId: numericId(stringAt(node, "author", "id")),
|
|
111
|
+
// The query is scoped to this project, so every node it returns is
|
|
112
|
+
// in it. R-1 clause 4 still compares — the rule lives in one place.
|
|
113
|
+
containerId: projectId,
|
|
114
|
+
categoryId: stringAt(node, "workItemType", "id"),
|
|
115
|
+
};
|
|
116
|
+
const authorized = authorizeThread(observed, {
|
|
117
|
+
trustedBots: config.trustedBots,
|
|
118
|
+
containerId: projectId,
|
|
119
|
+
categoryId: typeId,
|
|
120
|
+
});
|
|
121
|
+
if (authorized)
|
|
122
|
+
threads.push(authorized);
|
|
123
|
+
}
|
|
124
|
+
if (at(page, "pageInfo", "hasNextPage") !== true)
|
|
125
|
+
return threads;
|
|
126
|
+
cursor = stringAt(page, "pageInfo", "endCursor");
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
async create(ref) {
|
|
130
|
+
const { typeId } = await resolve();
|
|
131
|
+
const data = await graphql(config.api, headers, CREATE, {
|
|
132
|
+
path,
|
|
133
|
+
typeId,
|
|
134
|
+
title: ref,
|
|
135
|
+
body: threadBody(ref),
|
|
136
|
+
});
|
|
137
|
+
const item = at(data, "workItemCreate", "workItem");
|
|
138
|
+
const target = stringAt(item, "id");
|
|
139
|
+
if (target === "")
|
|
140
|
+
throw new ForgeError(`${path}: workItemCreate returned no work item`);
|
|
141
|
+
// Same contract as the GitHub side: `lockThreads: true` promises replies
|
|
142
|
+
// are refused, so failing to lock fails the run rather than quietly
|
|
143
|
+
// opening a forum the operator did not ask to moderate.
|
|
144
|
+
if (config.lockThreads) {
|
|
145
|
+
await graphql(config.api, headers, LOCK, { id: target });
|
|
146
|
+
}
|
|
147
|
+
// A fresh work item has no reactions, and asking for them would cost a
|
|
148
|
+
// second round trip to learn zero.
|
|
149
|
+
return { ref, target, url: stringAt(item, "webUrl"), up: 0 };
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
//# sourceMappingURL=provider_gitlab.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider_gitlab.js","sourceRoot":"","sources":["../../src/ratings/provider_gitlab.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The in-memory `RatingProvider` — the primary local test seam.
|
|
3
|
+
*
|
|
4
|
+
* It proves the reconcile logic with no network and no fixtures: threads live
|
|
5
|
+
* in an array, `create` appends to it, and the next `listAuthored` sees them.
|
|
6
|
+
* That is what makes "a rerun is idempotent" a thing a test can assert rather
|
|
7
|
+
* than a thing a comment claims.
|
|
8
|
+
*
|
|
9
|
+
* R-1 is not re-applied here. Every thread the fake holds is one it authored,
|
|
10
|
+
* which is exactly the post-R-1 state a live provider hands back.
|
|
11
|
+
*/
|
|
12
|
+
import { type RatingProvider, type RatingThread } from "./provider.js";
|
|
13
|
+
export interface MemoryProviderOptions {
|
|
14
|
+
/** Threads already on the fake forge. Repeat a `ref` to stage a conflict. */
|
|
15
|
+
threads?: RatingThread[];
|
|
16
|
+
/** Stop the listing pass after this many threads and raise a rate limit. */
|
|
17
|
+
listLimit?: number;
|
|
18
|
+
/** Raise a rate limit on the create after this many succeeded. */
|
|
19
|
+
createLimit?: number;
|
|
20
|
+
}
|
|
21
|
+
export interface MemoryProvider extends RatingProvider {
|
|
22
|
+
/** Live view of the fake forge — assert on it after a run. */
|
|
23
|
+
readonly threads: RatingThread[];
|
|
24
|
+
/** Targets of threads created with locking asked for. */
|
|
25
|
+
readonly locked: string[];
|
|
26
|
+
}
|
|
27
|
+
export declare function memoryProvider(options?: MemoryProviderOptions): MemoryProvider;
|
|
28
|
+
//# sourceMappingURL=provider_memory.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider_memory.d.ts","sourceRoot":"","sources":["../../src/ratings/provider_memory.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;GAUG;AAEH,OAAO,EAAe,KAAK,cAAc,EAAE,KAAK,YAAY,EAAE,MAAM,eAAe,CAAC;AAEpF,MAAM,WAAW,qBAAqB;IACpC,6EAA6E;IAC7E,OAAO,CAAC,EAAE,YAAY,EAAE,CAAC;IACzB,4EAA4E;IAC5E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kEAAkE;IAClE,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,cAAe,SAAQ,cAAc;IACpD,8DAA8D;IAC9D,QAAQ,CAAC,OAAO,EAAE,YAAY,EAAE,CAAC;IACjC,yDAAyD;IACzD,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC;CAC3B;AAED,wBAAgB,cAAc,CAAC,OAAO,GAAE,qBAA0B,GAAG,cAAc,CAsClF"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright 2026 The Grimoire Authors
|
|
3
|
+
/**
|
|
4
|
+
* The in-memory `RatingProvider` — the primary local test seam.
|
|
5
|
+
*
|
|
6
|
+
* It proves the reconcile logic with no network and no fixtures: threads live
|
|
7
|
+
* in an array, `create` appends to it, and the next `listAuthored` sees them.
|
|
8
|
+
* That is what makes "a rerun is idempotent" a thing a test can assert rather
|
|
9
|
+
* than a thing a comment claims.
|
|
10
|
+
*
|
|
11
|
+
* R-1 is not re-applied here. Every thread the fake holds is one it authored,
|
|
12
|
+
* which is exactly the post-R-1 state a live provider hands back.
|
|
13
|
+
*/
|
|
14
|
+
import { RateLimited } from "./provider.js";
|
|
15
|
+
export function memoryProvider(options = {}) {
|
|
16
|
+
const threads = options.threads ?? [];
|
|
17
|
+
const locked = [];
|
|
18
|
+
// The fake's limits are per-run, and a run always starts by listing — so the
|
|
19
|
+
// create counter resets there rather than needing a separate reset call.
|
|
20
|
+
let created = 0;
|
|
21
|
+
return {
|
|
22
|
+
threads,
|
|
23
|
+
locked,
|
|
24
|
+
listAuthored() {
|
|
25
|
+
created = 0;
|
|
26
|
+
const { listLimit } = options;
|
|
27
|
+
if (listLimit !== undefined && listLimit < threads.length) {
|
|
28
|
+
return Promise.reject(new RateLimited("memory: secondary rate limit while listing", 60_000, threads.slice(0, listLimit)));
|
|
29
|
+
}
|
|
30
|
+
return Promise.resolve([...threads]);
|
|
31
|
+
},
|
|
32
|
+
create(ref) {
|
|
33
|
+
if (options.createLimit !== undefined && created >= options.createLimit) {
|
|
34
|
+
return Promise.reject(new RateLimited("memory: secondary rate limit while creating", 60_000));
|
|
35
|
+
}
|
|
36
|
+
created += 1;
|
|
37
|
+
const thread = {
|
|
38
|
+
ref,
|
|
39
|
+
target: `t-${ref}`,
|
|
40
|
+
url: `https://forge.example/${ref}`,
|
|
41
|
+
up: 0,
|
|
42
|
+
};
|
|
43
|
+
threads.push(thread);
|
|
44
|
+
locked.push(thread.target);
|
|
45
|
+
return Promise.resolve(thread);
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=provider_memory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider_memory.js","sourceRoot":"","sources":["../../src/ratings/provider_memory.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,sCAAsC;AAEtC;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,WAAW,EAA0C,MAAM,eAAe,CAAC;AAkBpF,MAAM,UAAU,cAAc,CAAC,UAAiC,EAAE;IAChE,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC;IACtC,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,6EAA6E;IAC7E,yEAAyE;IACzE,IAAI,OAAO,GAAG,CAAC,CAAC;IAEhB,OAAO;QACL,OAAO;QACP,MAAM;QAEN,YAAY;YACV,OAAO,GAAG,CAAC,CAAC;YACZ,MAAM,EAAE,SAAS,EAAE,GAAG,OAAO,CAAC;YAC9B,IAAI,SAAS,KAAK,SAAS,IAAI,SAAS,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;gBAC1D,OAAO,OAAO,CAAC,MAAM,CACnB,IAAI,WAAW,CAAC,4CAA4C,EAAE,MAAM,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,CACnG,CAAC;YACJ,CAAC;YACD,OAAO,OAAO,CAAC,OAAO,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;QACvC,CAAC;QAED,MAAM,CAAC,GAAG;YACR,IAAI,OAAO,CAAC,WAAW,KAAK,SAAS,IAAI,OAAO,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;gBACxE,OAAO,OAAO,CAAC,MAAM,CAAC,IAAI,WAAW,CAAC,6CAA6C,EAAE,MAAM,CAAC,CAAC,CAAC;YAChG,CAAC;YACD,OAAO,IAAI,CAAC,CAAC;YACb,MAAM,MAAM,GAAiB;gBAC3B,GAAG;gBACH,MAAM,EAAE,KAAK,GAAG,EAAE;gBAClB,GAAG,EAAE,yBAAyB,GAAG,EAAE;gBACnC,EAAE,EAAE,CAAC;aACN,CAAC;YACF,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;YACrB,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;YAC3B,OAAO,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QACjC,CAAC;KACF,CAAC;AACJ,CAAC"}
|