@grimoire-rs/indexer 0.3.2 → 0.4.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 +137 -2
- 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/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 +8 -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/enrich/index.d.ts.map +1 -1
- package/dist/enrich/index.js +74 -1
- 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/provider.d.ts +108 -0
- package/dist/ratings/provider.d.ts.map +1 -0
- package/dist/ratings/provider.js +202 -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 +134 -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 +152 -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 +96 -28
- package/dist/renderer/astro/components/Catalog.tsx +136 -32
- package/dist/renderer/astro/layouts/Base.astro +15 -5
- 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 +146 -3
- 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 +6 -1
- package/dist/validate/adapters/http.d.ts.map +1 -1
- package/dist/validate/adapters/http.js +5 -1
- package/dist/validate/adapters/http.js.map +1 -1
- package/package.json +2 -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-ratings-seed.sh +83 -0
- package/templates/ci/gitlab-ratings.yml +37 -0
- package/templates/gitignore +8 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/ratings/config.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Invariant R-1 — marker authority.
|
|
3
|
+
*
|
|
4
|
+
* A `<!-- grim-ref: <ref> -->` marker binds `ref → thread` only when all four
|
|
5
|
+
* clauses hold: it is in the body of a top-level thread (never a comment,
|
|
6
|
+
* reply or note); the author's *account id* is registered in
|
|
7
|
+
* `index-policy.json`'s `trustedBots[].id`; it is the first anchored match in
|
|
8
|
+
* that body; and the thread still lives in the configured container — the
|
|
9
|
+
* repository/project id *and* the category or work-item type, compared by
|
|
10
|
+
* immutable id, never by name. A ref bound by more than one authorized thread
|
|
11
|
+
* is a conflict and contributes nothing.
|
|
12
|
+
*
|
|
13
|
+
* Pure, and separate from the providers because the rule must be enforced
|
|
14
|
+
* identically on both forges — two copies is how one of them drifts. The
|
|
15
|
+
* providers apply it inside `listAuthored()`, so no caller can receive an
|
|
16
|
+
* unauthorized thread and forget to filter it.
|
|
17
|
+
*/
|
|
18
|
+
import type { TrustedBot } from "../validate/core/ownership.js";
|
|
19
|
+
/** What R-1 compares against: the registered bots and the configured container. */
|
|
20
|
+
export interface MarkerPolicy {
|
|
21
|
+
/**
|
|
22
|
+
* `index-policy.json`'s `trustedBots`. Read for ids only — the bare-string
|
|
23
|
+
* form and the object form without an `id` are login-only and authorize
|
|
24
|
+
* nothing here, because clause 2 has an account id and no login in hand.
|
|
25
|
+
*/
|
|
26
|
+
trustedBots?: readonly TrustedBot[];
|
|
27
|
+
/** GitHub `repository.id` / GitLab `project.id`. */
|
|
28
|
+
containerId: string;
|
|
29
|
+
/** GitHub discussion category id / GitLab work-item type id. */
|
|
30
|
+
categoryId: string;
|
|
31
|
+
}
|
|
32
|
+
/** A forge thread as observed, before R-1 has ruled on it. */
|
|
33
|
+
export interface ObservedThread {
|
|
34
|
+
/** Opaque forge node id the vote mutation targets. */
|
|
35
|
+
target: string;
|
|
36
|
+
url: string;
|
|
37
|
+
/** The forge's own upvote counter. */
|
|
38
|
+
up: number;
|
|
39
|
+
body: string;
|
|
40
|
+
/** Absent when the forge reports no author — a deleted account. */
|
|
41
|
+
authorId?: string;
|
|
42
|
+
containerId: string;
|
|
43
|
+
categoryId: string;
|
|
44
|
+
/**
|
|
45
|
+
* Replies, comments or notes. Carried so clause 1 is enforced *here* rather
|
|
46
|
+
* than by each provider remembering not to pass them: this module never
|
|
47
|
+
* reads them, and an attacker's reply is the forgery clause 1 exists for.
|
|
48
|
+
*/
|
|
49
|
+
comments?: readonly {
|
|
50
|
+
body: string;
|
|
51
|
+
authorId?: string;
|
|
52
|
+
}[];
|
|
53
|
+
}
|
|
54
|
+
/** A thread R-1 accepted. Structurally the provider's `RatingThread`. */
|
|
55
|
+
export interface AuthorizedThread {
|
|
56
|
+
ref: string;
|
|
57
|
+
target: string;
|
|
58
|
+
url: string;
|
|
59
|
+
up: number;
|
|
60
|
+
}
|
|
61
|
+
/** A ref claimed by more than one authorized thread. Contributes zero votes. */
|
|
62
|
+
export interface RefConflict {
|
|
63
|
+
ref: string;
|
|
64
|
+
urls: string[];
|
|
65
|
+
}
|
|
66
|
+
export interface Resolution {
|
|
67
|
+
/** Refs bound by exactly one authorized thread. */
|
|
68
|
+
bound: Map<string, AuthorizedThread>;
|
|
69
|
+
conflicts: RefConflict[];
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The marker line for `ref`.
|
|
73
|
+
*
|
|
74
|
+
* Defence in depth against the injection `desiredRefs` already rejects at
|
|
75
|
+
* ingest: this function composes the body that {@link parseMarkerRef} later
|
|
76
|
+
* reads back, so a `ref` carrying a newline, a space, or a `-->` would author
|
|
77
|
+
* a *second* marker line under the bot's own identity — and every R-1 clause
|
|
78
|
+
* would pass it, because they constrain who wrote the body and where the
|
|
79
|
+
* thread lives, never what is in it. A ref that does not round-trip through
|
|
80
|
+
* the marker grammar is refused here rather than published.
|
|
81
|
+
*
|
|
82
|
+
* @throws {RangeError} when `ref` cannot survive its own marker.
|
|
83
|
+
*/
|
|
84
|
+
export declare function buildMarker(ref: string): string;
|
|
85
|
+
/**
|
|
86
|
+
* The first anchored marker in a body, by lowest byte offset — clause 3.
|
|
87
|
+
*
|
|
88
|
+
* Scanned line by line rather than with a `/m` regex: `^`/`$` alone would
|
|
89
|
+
* accept a marker sitting at column 0 inside a fenced code block, and would
|
|
90
|
+
* reject every marker in a CRLF body — which is what GitHub hands back for a
|
|
91
|
+
* discussion it stored. An unterminated fence swallows the rest of the body;
|
|
92
|
+
* that fails closed, which is the direction this rule errs in.
|
|
93
|
+
*/
|
|
94
|
+
export declare function parseMarkerRef(body: string): string | null;
|
|
95
|
+
/** Apply R-1 to one observed thread. `null` means it binds nothing. */
|
|
96
|
+
export declare function authorizeThread(thread: ObservedThread, policy: MarkerPolicy): AuthorizedThread | null;
|
|
97
|
+
/**
|
|
98
|
+
* Group authorized threads by ref. A ref claimed twice is ambiguous, and
|
|
99
|
+
* silently picking one would let a second thread quietly retarget a popular
|
|
100
|
+
* artifact's votes — so it contributes zero and the operator is told to delete
|
|
101
|
+
* all but one.
|
|
102
|
+
*/
|
|
103
|
+
export declare function resolveConflicts(threads: readonly AuthorizedThread[]): Resolution;
|
|
104
|
+
export declare function conflictWarning(conflict: RefConflict): string;
|
|
105
|
+
//# sourceMappingURL=marker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"marker.d.ts","sourceRoot":"","sources":["../../src/ratings/marker.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAC;AAEhE,mFAAmF;AACnF,MAAM,WAAW,YAAY;IAC3B;;;;OAIG;IACH,WAAW,CAAC,EAAE,SAAS,UAAU,EAAE,CAAC;IACpC,oDAAoD;IACpD,WAAW,EAAE,MAAM,CAAC;IACpB,gEAAgE;IAChE,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,8DAA8D;AAC9D,MAAM,WAAW,cAAc;IAC7B,sDAAsD;IACtD,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,sCAAsC;IACtC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,mEAAmE;IACnE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,SAAS;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAC3D;AAED,yEAAyE;AACzE,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,gFAAgF;AAChF,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED,MAAM,WAAW,UAAU;IACzB,mDAAmD;IACnD,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IACrC,SAAS,EAAE,WAAW,EAAE,CAAC;CAC1B;AAqBD;;;;;;;;;;;;GAYG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAQ/C;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAwB1D;AA0BD,uEAAuE;AACvE,wBAAgB,eAAe,CAC7B,MAAM,EAAE,cAAc,EACtB,MAAM,EAAE,YAAY,GACnB,gBAAgB,GAAG,IAAI,CAUzB;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,SAAS,gBAAgB,EAAE,GAAG,UAAU,CAgBjF;AAED,wBAAgB,eAAe,CAAC,QAAQ,EAAE,WAAW,GAAG,MAAM,CAE7D"}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright 2026 The Grimoire Authors
|
|
3
|
+
/**
|
|
4
|
+
* Anchored: the whole line and nothing else. Not because an unanchored pattern
|
|
5
|
+
* is exploitable while clause 2 holds — an attacker's content is never parsed
|
|
6
|
+
* as a marker source — but because an unanchored parser is a parser-
|
|
7
|
+
* differential bug waiting for the day someone relaxes clause 2.
|
|
8
|
+
*/
|
|
9
|
+
const MARKER_LINE = /^<!-- grim-ref: (\S+) -->$/;
|
|
10
|
+
/**
|
|
11
|
+
* A fence opener or closer: the run of `` ` `` or `~` and whatever follows it.
|
|
12
|
+
*
|
|
13
|
+
* CommonMark closes a fence only with **the same character**, in a run at
|
|
14
|
+
* least as long as the opener — so a `~~~` inside a ``` block is ordinary
|
|
15
|
+
* text, not a close. A boolean toggle gets that wrong in the direction that
|
|
16
|
+
* matters: ``` then `~~~` then a marker would read the marker as unfenced and
|
|
17
|
+
* bind it.
|
|
18
|
+
*/
|
|
19
|
+
const FENCE = /^\s{0,3}(`{3,}|~{3,})/;
|
|
20
|
+
/**
|
|
21
|
+
* The marker line for `ref`.
|
|
22
|
+
*
|
|
23
|
+
* Defence in depth against the injection `desiredRefs` already rejects at
|
|
24
|
+
* ingest: this function composes the body that {@link parseMarkerRef} later
|
|
25
|
+
* reads back, so a `ref` carrying a newline, a space, or a `-->` would author
|
|
26
|
+
* a *second* marker line under the bot's own identity — and every R-1 clause
|
|
27
|
+
* would pass it, because they constrain who wrote the body and where the
|
|
28
|
+
* thread lives, never what is in it. A ref that does not round-trip through
|
|
29
|
+
* the marker grammar is refused here rather than published.
|
|
30
|
+
*
|
|
31
|
+
* @throws {RangeError} when `ref` cannot survive its own marker.
|
|
32
|
+
*/
|
|
33
|
+
export function buildMarker(ref) {
|
|
34
|
+
const line = `<!-- grim-ref: ${ref} -->`;
|
|
35
|
+
if (MARKER_LINE.exec(line)?.[1] !== ref) {
|
|
36
|
+
throw new RangeError(`refusing to build a rating marker for ${JSON.stringify(ref)}: it does not round-trip through the marker grammar`);
|
|
37
|
+
}
|
|
38
|
+
return line;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The first anchored marker in a body, by lowest byte offset — clause 3.
|
|
42
|
+
*
|
|
43
|
+
* Scanned line by line rather than with a `/m` regex: `^`/`$` alone would
|
|
44
|
+
* accept a marker sitting at column 0 inside a fenced code block, and would
|
|
45
|
+
* reject every marker in a CRLF body — which is what GitHub hands back for a
|
|
46
|
+
* discussion it stored. An unterminated fence swallows the rest of the body;
|
|
47
|
+
* that fails closed, which is the direction this rule errs in.
|
|
48
|
+
*/
|
|
49
|
+
export function parseMarkerRef(body) {
|
|
50
|
+
// The open fence's delimiter run, or null outside a fence. Tracked as the
|
|
51
|
+
// run itself rather than a boolean so a different fence character, or a
|
|
52
|
+
// shorter run of the same one, cannot close it (CommonMark 4.5).
|
|
53
|
+
let open = null;
|
|
54
|
+
for (const line of body.split(/\r?\n/)) {
|
|
55
|
+
const fence = FENCE.exec(line)?.[1];
|
|
56
|
+
if (fence) {
|
|
57
|
+
if (open === null) {
|
|
58
|
+
open = fence;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (fence[0] === open[0] && fence.length >= open.length) {
|
|
62
|
+
open = null;
|
|
63
|
+
}
|
|
64
|
+
// A non-matching run inside a fence is content, and content is never
|
|
65
|
+
// a marker source.
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (open !== null)
|
|
69
|
+
continue;
|
|
70
|
+
const ref = MARKER_LINE.exec(line)?.[1];
|
|
71
|
+
if (ref)
|
|
72
|
+
return ref;
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Is this author one of the registered bots? Clause 2, keyed by account id.
|
|
78
|
+
*
|
|
79
|
+
* `findTrustedBot` cannot serve this — it is login-keyed, and R-1 has an id
|
|
80
|
+
* and no login. An entry without an `id` is skipped, never read as a wildcard;
|
|
81
|
+
* an author without an id matches nothing, so a deleted account and an
|
|
82
|
+
* unpinned entry cannot meet in the middle as `undefined === undefined`.
|
|
83
|
+
*/
|
|
84
|
+
function authorIsTrustedBot(bots, authorId) {
|
|
85
|
+
if (authorId === undefined)
|
|
86
|
+
return false;
|
|
87
|
+
return (bots ?? []).some(
|
|
88
|
+
// `index-policy.json` is hand-written and `id` is *typed* string but never
|
|
89
|
+
// validated at runtime, so an operator who writes `"id": 99` — which this
|
|
90
|
+
// repo's own fixtures do — used to fail `99 === "99"` on every thread,
|
|
91
|
+
// authorize nothing, and publish an empty rating set. Compare in string
|
|
92
|
+
// space so the natural thing to type works. A value that is not
|
|
93
|
+
// id-shaped still matches nothing, which is the safe direction.
|
|
94
|
+
(bot) => typeof bot !== "string" && bot.id !== undefined && String(bot.id) === authorId);
|
|
95
|
+
}
|
|
96
|
+
/** Apply R-1 to one observed thread. `null` means it binds nothing. */
|
|
97
|
+
export function authorizeThread(thread, policy) {
|
|
98
|
+
// Clause 4, two independent equalities: a transfer moves the container id, a
|
|
99
|
+
// discussion→issue conversion moves the category. Either alone disqualifies.
|
|
100
|
+
if (thread.containerId !== policy.containerId)
|
|
101
|
+
return null;
|
|
102
|
+
if (thread.categoryId !== policy.categoryId)
|
|
103
|
+
return null;
|
|
104
|
+
if (!authorIsTrustedBot(policy.trustedBots, thread.authorId))
|
|
105
|
+
return null;
|
|
106
|
+
// Clause 1: the thread's own body. `thread.comments` is deliberately unread.
|
|
107
|
+
const ref = parseMarkerRef(thread.body);
|
|
108
|
+
if (ref === null)
|
|
109
|
+
return null;
|
|
110
|
+
return { ref, target: thread.target, url: thread.url, up: thread.up };
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Group authorized threads by ref. A ref claimed twice is ambiguous, and
|
|
114
|
+
* silently picking one would let a second thread quietly retarget a popular
|
|
115
|
+
* artifact's votes — so it contributes zero and the operator is told to delete
|
|
116
|
+
* all but one.
|
|
117
|
+
*/
|
|
118
|
+
export function resolveConflicts(threads) {
|
|
119
|
+
const byRef = new Map();
|
|
120
|
+
for (const thread of threads) {
|
|
121
|
+
const group = byRef.get(thread.ref);
|
|
122
|
+
if (group)
|
|
123
|
+
group.push(thread);
|
|
124
|
+
else
|
|
125
|
+
byRef.set(thread.ref, [thread]);
|
|
126
|
+
}
|
|
127
|
+
const bound = new Map();
|
|
128
|
+
const conflicts = [];
|
|
129
|
+
for (const [ref, group] of byRef) {
|
|
130
|
+
const only = group.length === 1 ? group[0] : undefined;
|
|
131
|
+
if (only)
|
|
132
|
+
bound.set(ref, only);
|
|
133
|
+
else
|
|
134
|
+
conflicts.push({ ref, urls: group.map((thread) => thread.url) });
|
|
135
|
+
}
|
|
136
|
+
return { bound, conflicts };
|
|
137
|
+
}
|
|
138
|
+
export function conflictWarning(conflict) {
|
|
139
|
+
return `ratings: ref ${conflict.ref} bound by ${conflict.urls.length} threads: ${conflict.urls.join(", ")} — delete all but one`;
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=marker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"marker.js","sourceRoot":"","sources":["../../src/ratings/marker.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `RatingProvider` seam — one interface, two forges, one in-memory fake.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors `validate/adapters/forge.ts`: an interface, per-forge closures, and a
|
|
5
|
+
* `create*` factory that picks between them. The GraphQL plumbing every
|
|
6
|
+
* provider needs lives here rather than in each of them, because the one rule
|
|
7
|
+
* that must not drift is [`graphql`]'s error branching — a 200 carrying a
|
|
8
|
+
* populated `errors` array is a hard error, and two copies of that check is how
|
|
9
|
+
* one of them turns into an empty tally.
|
|
10
|
+
*/
|
|
11
|
+
import { CliError, type ExitCode } from "../cli/exit.js";
|
|
12
|
+
import { type AuthorizedThread } from "./marker.js";
|
|
13
|
+
import type { TrustedBot } from "../validate/core/ownership.js";
|
|
14
|
+
/**
|
|
15
|
+
* A thread the reconcile loop may tally. Declared as an alias of R-1's
|
|
16
|
+
* `AuthorizedThread` rather than a second identical interface: R-1 is what
|
|
17
|
+
* produces these, and the type should say so.
|
|
18
|
+
*/
|
|
19
|
+
export type RatingThread = AuthorizedThread;
|
|
20
|
+
export interface RatingProviderConfig {
|
|
21
|
+
/** GraphQL endpoint, e.g. `https://api.github.com/graphql`. */
|
|
22
|
+
api: string;
|
|
23
|
+
/** Credential with write access to threads. Never logged. */
|
|
24
|
+
token: string;
|
|
25
|
+
/** GitHub `owner/repo`; GitLab full project path. */
|
|
26
|
+
project: string;
|
|
27
|
+
/** GitHub Discussions category name; GitLab work item type name. */
|
|
28
|
+
container: string;
|
|
29
|
+
/** Lock threads on creation — votes count, replies are refused. */
|
|
30
|
+
lockThreads: boolean;
|
|
31
|
+
/** `index-policy.json`'s allowlist. R-1 clause 2 reads ids out of it. */
|
|
32
|
+
trustedBots?: readonly TrustedBot[];
|
|
33
|
+
}
|
|
34
|
+
export interface RatingProvider {
|
|
35
|
+
/**
|
|
36
|
+
* Every thread the bot authored in the configured container, paginated to
|
|
37
|
+
* exhaustion. R-1 filtering is applied **here**, never by the caller.
|
|
38
|
+
*
|
|
39
|
+
* A secondary rate limit part-way through throws [`RateLimited`] carrying the
|
|
40
|
+
* pages already read — a truncated pass is partial, not empty, and the caller
|
|
41
|
+
* needs both facts to publish without wiping what it could not observe.
|
|
42
|
+
*/
|
|
43
|
+
listAuthored(): Promise<RatingThread[]>;
|
|
44
|
+
/** Create one thread carrying the marker. The budget is the caller's job. */
|
|
45
|
+
create(ref: string): Promise<RatingThread>;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The forge answered, and the answer was unusable: a transport failure, a
|
|
49
|
+
* non-2xx, a body that did not parse, or a 200 carrying GraphQL `errors`.
|
|
50
|
+
*
|
|
51
|
+
* A `CliError` subclass rather than a name in `classify()`'s list, following
|
|
52
|
+
* `seed.ts`: that list exists for errors thrown by modules `main.ts` must not
|
|
53
|
+
* import, and `cli/exit.js` is already one of its static imports — so the
|
|
54
|
+
* mapping here is by type, which cannot drift out of sync with a string.
|
|
55
|
+
*/
|
|
56
|
+
export declare class ForgeError extends CliError {
|
|
57
|
+
constructor(message: string, code?: ExitCode);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* A secondary rate limit. Distinguished from [`ForgeError`] because the two are
|
|
61
|
+
* handled oppositely: a forge error fails the run, a rate limit stops it early
|
|
62
|
+
* and publishes what it already got.
|
|
63
|
+
*/
|
|
64
|
+
export declare class RateLimited extends ForgeError {
|
|
65
|
+
/** What the forge asked us to wait, by GitHub's documented precedence. */
|
|
66
|
+
readonly retryAfterMs: number;
|
|
67
|
+
/** Threads read before the limit. Empty on the create path. */
|
|
68
|
+
readonly observed: readonly RatingThread[];
|
|
69
|
+
constructor(message: string, retryAfterMs: number, observed?: readonly RatingThread[]);
|
|
70
|
+
}
|
|
71
|
+
/** GitHub's documented floor when neither header says how long to wait. */
|
|
72
|
+
export declare const BACKOFF_FLOOR_MS = 60000;
|
|
73
|
+
/**
|
|
74
|
+
* Nodes per page.
|
|
75
|
+
*
|
|
76
|
+
* ponytail: 50 rather than the API maximum of 100 because `request()` caps a
|
|
77
|
+
* response at 1 MiB and a page carries every thread's full body — including
|
|
78
|
+
* bodies this bot did not write. Halve it again, or teach `request()` a
|
|
79
|
+
* per-call cap, if an index ever trips the cap on a legitimate page.
|
|
80
|
+
*/
|
|
81
|
+
export declare const PAGE_SIZE = 50;
|
|
82
|
+
/**
|
|
83
|
+
* The body every provider writes. The marker sits on its own line, which is the
|
|
84
|
+
* only position R-1's anchored parser accepts; the prose above it is for the
|
|
85
|
+
* human who lands on the thread from the catalog.
|
|
86
|
+
*/
|
|
87
|
+
export declare function threadBody(ref: string): string;
|
|
88
|
+
export declare function field(data: unknown, key: string): unknown;
|
|
89
|
+
/** `field`, chained. Every GraphQL read here is a path into an `unknown`. */
|
|
90
|
+
export declare function at(data: unknown, ...keys: string[]): unknown;
|
|
91
|
+
export declare function stringAt(data: unknown, ...keys: string[]): string;
|
|
92
|
+
export declare function nodes(data: unknown): unknown[];
|
|
93
|
+
/**
|
|
94
|
+
* One GraphQL round trip, with the branching every provider shares.
|
|
95
|
+
*
|
|
96
|
+
* Order matters and is the reason this is one function: a rate limit is
|
|
97
|
+
* classified first (it is the one failure that must not fail the run), then
|
|
98
|
+
* `errors` is checked **independently of the HTTP status and before `data` is
|
|
99
|
+
* touched** — a 200 with `errors` and a null `data` otherwise reads as
|
|
100
|
+
* "genuinely fewer votes observed", which is silent emptying arriving through
|
|
101
|
+
* the one door R-2 does not watch.
|
|
102
|
+
*
|
|
103
|
+
* @param observed Threads already read, attached to a [`RateLimited`] so a
|
|
104
|
+
* truncated pagination pass stays partial rather than becoming empty.
|
|
105
|
+
*/
|
|
106
|
+
export declare function graphql(api: string, headers: Record<string, string>, query: string, variables: Record<string, unknown>, observed?: readonly RatingThread[]): Promise<Record<string, unknown>>;
|
|
107
|
+
export declare function createRatingProvider(kind: "github" | "gitlab", config: RatingProviderConfig): RatingProvider;
|
|
108
|
+
//# sourceMappingURL=provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../../src/ratings/provider.ts"],"names":[],"mappings":"AAGA;;;;;;;;;GASG;AAEH,OAAO,EAAE,QAAQ,EAAQ,KAAK,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE/D,OAAO,EAAe,KAAK,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACjE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAC;AAOhE;;;;GAIG;AACH,MAAM,MAAM,YAAY,GAAG,gBAAgB,CAAC;AAE5C,MAAM,WAAW,oBAAoB;IACnC,+DAA+D;IAC/D,GAAG,EAAE,MAAM,CAAC;IACZ,6DAA6D;IAC7D,KAAK,EAAE,MAAM,CAAC;IACd,qDAAqD;IACrD,OAAO,EAAE,MAAM,CAAC;IAChB,oEAAoE;IACpE,SAAS,EAAE,MAAM,CAAC;IAClB,mEAAmE;IACnE,WAAW,EAAE,OAAO,CAAC;IACrB,yEAAyE;IACzE,WAAW,CAAC,EAAE,SAAS,UAAU,EAAE,CAAC;CACrC;AAED,MAAM,WAAW,cAAc;IAC7B;;;;;;;OAOG;IACH,YAAY,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC;IACxC,6EAA6E;IAC7E,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;CAC5C;AAED;;;;;;;;GAQG;AACH,qBAAa,UAAW,SAAQ,QAAQ;gBAC1B,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,QAA2B;CAI/D;AAED;;;;GAIG;AACH,qBAAa,WAAY,SAAQ,UAAU;IACzC,0EAA0E;IAC1E,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,+DAA+D;IAC/D,QAAQ,CAAC,QAAQ,EAAE,SAAS,YAAY,EAAE,CAAC;gBAE/B,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,QAAQ,GAAE,SAAS,YAAY,EAAO;CAM1F;AAED,2EAA2E;AAC3E,eAAO,MAAM,gBAAgB,QAAS,CAAC;AAEvC;;;;;;;GAOG;AACH,eAAO,MAAM,SAAS,KAAK,CAAC;AAE5B;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAQ9C;AAED,wBAAgB,KAAK,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAGzD;AAED,6EAA6E;AAC7E,wBAAgB,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAE5D;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,CAGjE;AAED,wBAAgB,KAAK,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,EAAE,CAG9C;AAqDD;;;;;;;;;;;;GAYG;AACH,wBAAsB,OAAO,CAC3B,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC/B,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAClC,QAAQ,GAAE,SAAS,YAAY,EAAO,GACrC,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CA8ClC;AAED,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,QAAQ,GAAG,QAAQ,EACzB,MAAM,EAAE,oBAAoB,GAC3B,cAAc,CAIhB"}
|
|
@@ -0,0 +1,202 @@
|
|
|
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 { 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
|
+
import { githubProvider as github } from "./provider_github.js";
|
|
20
|
+
import { gitlabProvider as gitlab } from "./provider_gitlab.js";
|
|
21
|
+
/**
|
|
22
|
+
* The forge answered, and the answer was unusable: a transport failure, a
|
|
23
|
+
* non-2xx, a body that did not parse, or a 200 carrying GraphQL `errors`.
|
|
24
|
+
*
|
|
25
|
+
* A `CliError` subclass rather than a name in `classify()`'s list, following
|
|
26
|
+
* `seed.ts`: that list exists for errors thrown by modules `main.ts` must not
|
|
27
|
+
* import, and `cli/exit.js` is already one of its static imports — so the
|
|
28
|
+
* mapping here is by type, which cannot drift out of sync with a string.
|
|
29
|
+
*/
|
|
30
|
+
export class ForgeError extends CliError {
|
|
31
|
+
constructor(message, code = EXIT.unavailable) {
|
|
32
|
+
super(message, code);
|
|
33
|
+
this.name = "RatingsForgeError";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* A secondary rate limit. Distinguished from [`ForgeError`] because the two are
|
|
38
|
+
* handled oppositely: a forge error fails the run, a rate limit stops it early
|
|
39
|
+
* and publishes what it already got.
|
|
40
|
+
*/
|
|
41
|
+
export class RateLimited extends ForgeError {
|
|
42
|
+
/** What the forge asked us to wait, by GitHub's documented precedence. */
|
|
43
|
+
retryAfterMs;
|
|
44
|
+
/** Threads read before the limit. Empty on the create path. */
|
|
45
|
+
observed;
|
|
46
|
+
constructor(message, retryAfterMs, observed = []) {
|
|
47
|
+
super(message);
|
|
48
|
+
this.name = "RatingsRateLimited";
|
|
49
|
+
this.retryAfterMs = retryAfterMs;
|
|
50
|
+
this.observed = observed;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** GitHub's documented floor when neither header says how long to wait. */
|
|
54
|
+
export const BACKOFF_FLOOR_MS = 60_000;
|
|
55
|
+
/**
|
|
56
|
+
* Nodes per page.
|
|
57
|
+
*
|
|
58
|
+
* ponytail: 50 rather than the API maximum of 100 because `request()` caps a
|
|
59
|
+
* response at 1 MiB and a page carries every thread's full body — including
|
|
60
|
+
* bodies this bot did not write. Halve it again, or teach `request()` a
|
|
61
|
+
* per-call cap, if an index ever trips the cap on a legitimate page.
|
|
62
|
+
*/
|
|
63
|
+
export const PAGE_SIZE = 50;
|
|
64
|
+
/**
|
|
65
|
+
* The body every provider writes. The marker sits on its own line, which is the
|
|
66
|
+
* only position R-1's anchored parser accepts; the prose above it is for the
|
|
67
|
+
* human who lands on the thread from the catalog.
|
|
68
|
+
*/
|
|
69
|
+
export function threadBody(ref) {
|
|
70
|
+
return [
|
|
71
|
+
`Rating thread for \`${ref}\`.`,
|
|
72
|
+
"",
|
|
73
|
+
"Upvote this thread to rate the package. Replies are not read by the tally.",
|
|
74
|
+
"",
|
|
75
|
+
buildMarker(ref),
|
|
76
|
+
].join("\n");
|
|
77
|
+
}
|
|
78
|
+
export function field(data, key) {
|
|
79
|
+
if (typeof data !== "object" || data === null)
|
|
80
|
+
return undefined;
|
|
81
|
+
return data[key];
|
|
82
|
+
}
|
|
83
|
+
/** `field`, chained. Every GraphQL read here is a path into an `unknown`. */
|
|
84
|
+
export function at(data, ...keys) {
|
|
85
|
+
return keys.reduce((value, key) => field(value, key), data);
|
|
86
|
+
}
|
|
87
|
+
export function stringAt(data, ...keys) {
|
|
88
|
+
const value = at(data, ...keys);
|
|
89
|
+
return typeof value === "string" ? value : "";
|
|
90
|
+
}
|
|
91
|
+
export function nodes(data) {
|
|
92
|
+
const value = at(data, "nodes");
|
|
93
|
+
return Array.isArray(value) ? value : [];
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* How long the forge asked us to wait: `retry-after` first, then
|
|
97
|
+
* `x-ratelimit-reset`, else the documented floor. GitHub's guidance is explicit
|
|
98
|
+
* that continuing to make requests while rate limited can get an integration
|
|
99
|
+
* banned, so this value is honoured by *stopping*, which is the strongest form
|
|
100
|
+
* of honouring it — a scheduled run resumes on the next tick, and sleeping here
|
|
101
|
+
* would burn CI minutes waiting to make requests the design has already decided
|
|
102
|
+
* not to make.
|
|
103
|
+
*/
|
|
104
|
+
function retryAfterMs(response) {
|
|
105
|
+
const after = Number(response.header("retry-after"));
|
|
106
|
+
if (Number.isFinite(after) && after > 0)
|
|
107
|
+
return after * 1000;
|
|
108
|
+
const reset = Number(response.header("x-ratelimit-reset"));
|
|
109
|
+
if (Number.isFinite(reset) && reset > 0) {
|
|
110
|
+
const wait = reset * 1000 - Date.now();
|
|
111
|
+
if (wait > 0)
|
|
112
|
+
return wait;
|
|
113
|
+
}
|
|
114
|
+
return BACKOFF_FLOOR_MS;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Is this reply a rate limit rather than an ordinary refusal?
|
|
118
|
+
*
|
|
119
|
+
* 429 always. 403 only when it says so — GitHub returns 403 for both a
|
|
120
|
+
* secondary limit and a plain permission failure, and the two are handled
|
|
121
|
+
* oppositely, so the discrimination has to be explicit.
|
|
122
|
+
*/
|
|
123
|
+
function rateLimitOf(response) {
|
|
124
|
+
if (response.status === 429)
|
|
125
|
+
return retryAfterMs(response);
|
|
126
|
+
if (response.status !== 403)
|
|
127
|
+
return null;
|
|
128
|
+
const secondary = /secondary rate limit|rate limit/i.test(response.body) ||
|
|
129
|
+
response.header("retry-after") !== "" ||
|
|
130
|
+
response.header("x-ratelimit-remaining") === "0";
|
|
131
|
+
return secondary ? retryAfterMs(response) : null;
|
|
132
|
+
}
|
|
133
|
+
function graphqlErrors(parsed) {
|
|
134
|
+
const errors = field(parsed, "errors");
|
|
135
|
+
if (!Array.isArray(errors) || errors.length === 0)
|
|
136
|
+
return null;
|
|
137
|
+
const message = errors
|
|
138
|
+
.map((err) => stringAt(err, "message") || JSON.stringify(err))
|
|
139
|
+
.join("; ");
|
|
140
|
+
const limited = errors.some((err) => stringAt(err, "type") === "RATE_LIMITED" ||
|
|
141
|
+
stringAt(err, "extensions", "code") === "RATE_LIMITED");
|
|
142
|
+
return { message, limited };
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* One GraphQL round trip, with the branching every provider shares.
|
|
146
|
+
*
|
|
147
|
+
* Order matters and is the reason this is one function: a rate limit is
|
|
148
|
+
* classified first (it is the one failure that must not fail the run), then
|
|
149
|
+
* `errors` is checked **independently of the HTTP status and before `data` is
|
|
150
|
+
* touched** — a 200 with `errors` and a null `data` otherwise reads as
|
|
151
|
+
* "genuinely fewer votes observed", which is silent emptying arriving through
|
|
152
|
+
* the one door R-2 does not watch.
|
|
153
|
+
*
|
|
154
|
+
* @param observed Threads already read, attached to a [`RateLimited`] so a
|
|
155
|
+
* truncated pagination pass stays partial rather than becoming empty.
|
|
156
|
+
*/
|
|
157
|
+
export async function graphql(api, headers, query, variables, observed = []) {
|
|
158
|
+
const response = await request(api, { ...headers, "Content-Type": "application/json" }, {
|
|
159
|
+
method: "POST",
|
|
160
|
+
body: JSON.stringify({ query, variables }),
|
|
161
|
+
});
|
|
162
|
+
const wait = rateLimitOf(response);
|
|
163
|
+
if (wait !== null) {
|
|
164
|
+
throw new RateLimited(`${api}: secondary rate limit (HTTP ${response.status}), retry after ${Math.round(wait / 1000)}s`, wait, observed);
|
|
165
|
+
}
|
|
166
|
+
let parsed;
|
|
167
|
+
let parseError = null;
|
|
168
|
+
try {
|
|
169
|
+
parsed = JSON.parse(response.body);
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
parseError = err.message;
|
|
173
|
+
}
|
|
174
|
+
const errors = graphqlErrors(parsed);
|
|
175
|
+
if (errors) {
|
|
176
|
+
if (errors.limited)
|
|
177
|
+
throw new RateLimited(`${api}: ${errors.message}`, BACKOFF_FLOOR_MS, observed);
|
|
178
|
+
throw new ForgeError(`${api}: ${errors.message}`);
|
|
179
|
+
}
|
|
180
|
+
// `status: 0` is `request()`'s transport catch *and* its response-size cap.
|
|
181
|
+
// Neither says anything about what the forge holds, so both are hard errors.
|
|
182
|
+
if (response.status === 0) {
|
|
183
|
+
throw new ForgeError(`${api}: the request did not complete (transport failure or oversized reply)`);
|
|
184
|
+
}
|
|
185
|
+
if (response.status < 200 || response.status >= 300) {
|
|
186
|
+
throw new ForgeError(`${api}: HTTP ${response.status}`);
|
|
187
|
+
}
|
|
188
|
+
if (parseError !== null) {
|
|
189
|
+
throw new ForgeError(`${api}: the reply did not parse (${parseError})`);
|
|
190
|
+
}
|
|
191
|
+
const data = field(parsed, "data");
|
|
192
|
+
if (typeof data !== "object" || data === null) {
|
|
193
|
+
throw new ForgeError(`${api}: the reply carried no data`);
|
|
194
|
+
}
|
|
195
|
+
return data;
|
|
196
|
+
}
|
|
197
|
+
export function createRatingProvider(kind, config) {
|
|
198
|
+
// Deliberately a `?:` and not a registry: two forges, one dispatch site, the
|
|
199
|
+
// same shape `createForge` already uses.
|
|
200
|
+
return kind === "github" ? github(config) : gitlab(config);
|
|
201
|
+
}
|
|
202
|
+
//# 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,EAQL,KAAK,cAAc,EACnB,KAAK,oBAAoB,EAE1B,MAAM,eAAe,CAAC;AAyCvB,wBAAgB,cAAc,CAAC,MAAM,EAAE,oBAAoB,GAAG,cAAc,CAiG3E"}
|