@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.
Files changed (110) hide show
  1. package/README.md +220 -9
  2. package/dist/ci.d.ts +6 -1
  3. package/dist/ci.d.ts.map +1 -1
  4. package/dist/ci.js +32 -1
  5. package/dist/ci.js.map +1 -1
  6. package/dist/cli/build.d.ts.map +1 -1
  7. package/dist/cli/build.js +3 -1
  8. package/dist/cli/build.js.map +1 -1
  9. package/dist/cli/ci.d.ts.map +1 -1
  10. package/dist/cli/ci.js +5 -1
  11. package/dist/cli/ci.js.map +1 -1
  12. package/dist/cli/enrich.d.ts +1 -0
  13. package/dist/cli/enrich.d.ts.map +1 -1
  14. package/dist/cli/enrich.js +38 -0
  15. package/dist/cli/enrich.js.map +1 -1
  16. package/dist/cli/init.d.ts.map +1 -1
  17. package/dist/cli/init.js +17 -3
  18. package/dist/cli/init.js.map +1 -1
  19. package/dist/cli/main.d.ts.map +1 -1
  20. package/dist/cli/main.js +9 -0
  21. package/dist/cli/main.js.map +1 -1
  22. package/dist/cli/ratings.d.ts +3 -0
  23. package/dist/cli/ratings.d.ts.map +1 -0
  24. package/dist/cli/ratings.js +160 -0
  25. package/dist/cli/ratings.js.map +1 -0
  26. package/dist/config.d.ts +19 -0
  27. package/dist/config.d.ts.map +1 -1
  28. package/dist/config.js +47 -1
  29. package/dist/config.js.map +1 -1
  30. package/dist/data/index.d.ts.map +1 -1
  31. package/dist/data/index.js +13 -0
  32. package/dist/data/index.js.map +1 -1
  33. package/dist/enrich/checkpoint.d.ts +53 -0
  34. package/dist/enrich/checkpoint.d.ts.map +1 -0
  35. package/dist/enrich/checkpoint.js +296 -0
  36. package/dist/enrich/checkpoint.js.map +1 -0
  37. package/dist/enrich/index.d.ts +14 -0
  38. package/dist/enrich/index.d.ts.map +1 -1
  39. package/dist/enrich/index.js +77 -4
  40. package/dist/enrich/index.js.map +1 -1
  41. package/dist/ratings/budget.d.ts +16 -0
  42. package/dist/ratings/budget.d.ts.map +1 -0
  43. package/dist/ratings/budget.js +7 -0
  44. package/dist/ratings/budget.js.map +1 -0
  45. package/dist/ratings/config.d.ts +56 -0
  46. package/dist/ratings/config.d.ts.map +1 -0
  47. package/dist/ratings/config.js +86 -0
  48. package/dist/ratings/config.js.map +1 -0
  49. package/dist/ratings/marker.d.ts +105 -0
  50. package/dist/ratings/marker.d.ts.map +1 -0
  51. package/dist/ratings/marker.js +141 -0
  52. package/dist/ratings/marker.js.map +1 -0
  53. package/dist/ratings/paging.d.ts +16 -0
  54. package/dist/ratings/paging.d.ts.map +1 -0
  55. package/dist/ratings/paging.js +36 -0
  56. package/dist/ratings/paging.js.map +1 -0
  57. package/dist/ratings/provider.d.ts +100 -0
  58. package/dist/ratings/provider.d.ts.map +1 -0
  59. package/dist/ratings/provider.js +203 -0
  60. package/dist/ratings/provider.js.map +1 -0
  61. package/dist/ratings/provider_github.d.ts +3 -0
  62. package/dist/ratings/provider_github.d.ts.map +1 -0
  63. package/dist/ratings/provider_github.js +135 -0
  64. package/dist/ratings/provider_github.js.map +1 -0
  65. package/dist/ratings/provider_gitlab.d.ts +3 -0
  66. package/dist/ratings/provider_gitlab.d.ts.map +1 -0
  67. package/dist/ratings/provider_gitlab.js +153 -0
  68. package/dist/ratings/provider_gitlab.js.map +1 -0
  69. package/dist/ratings/provider_memory.d.ts +28 -0
  70. package/dist/ratings/provider_memory.d.ts.map +1 -0
  71. package/dist/ratings/provider_memory.js +49 -0
  72. package/dist/ratings/provider_memory.js.map +1 -0
  73. package/dist/ratings/reconcile.d.ts +47 -0
  74. package/dist/ratings/reconcile.d.ts.map +1 -0
  75. package/dist/ratings/reconcile.js +133 -0
  76. package/dist/ratings/reconcile.js.map +1 -0
  77. package/dist/ratings/seed.d.ts +60 -0
  78. package/dist/ratings/seed.d.ts.map +1 -0
  79. package/dist/ratings/seed.js +132 -0
  80. package/dist/ratings/seed.js.map +1 -0
  81. package/dist/renderer/astro/components/Catalog.d.ts +2 -0
  82. package/dist/renderer/astro/components/Catalog.js +65 -16
  83. package/dist/renderer/astro/components/Catalog.tsx +119 -26
  84. package/dist/renderer/astro/components/CommandField.astro +1 -0
  85. package/dist/renderer/astro/components/VersionMenu.astro +31 -23
  86. package/dist/renderer/astro/layouts/Base.astro +218 -263
  87. package/dist/renderer/astro/lib/catalog.d.ts +25 -0
  88. package/dist/renderer/astro/lib/catalog.js +46 -0
  89. package/dist/renderer/astro/lib/catalog.ts +49 -0
  90. package/dist/renderer/astro/pages/p/[...slug].astro +238 -85
  91. package/dist/renderer/astro/styles/tokens.css +157 -0
  92. package/dist/renderer/index.d.ts.map +1 -1
  93. package/dist/renderer/index.js +101 -2
  94. package/dist/renderer/index.js.map +1 -1
  95. package/dist/renderer/types.d.ts +41 -0
  96. package/dist/renderer/types.d.ts.map +1 -1
  97. package/dist/validate/adapters/http.d.ts +29 -1
  98. package/dist/validate/adapters/http.d.ts.map +1 -1
  99. package/dist/validate/adapters/http.js +17 -2
  100. package/dist/validate/adapters/http.js.map +1 -1
  101. package/package.json +1 -1
  102. package/templates/ci/github-enrich.yml +9 -1
  103. package/templates/ci/github-pages.yml +11 -5
  104. package/templates/ci/github-ratings-seed.yml +88 -0
  105. package/templates/ci/github-ratings.yml +51 -0
  106. package/templates/ci/gitlab-ci.yml +3 -3
  107. package/templates/ci/gitlab-enrich.sh +10 -1
  108. package/templates/ci/gitlab-ratings-seed.sh +83 -0
  109. package/templates/ci/gitlab-ratings.yml +37 -0
  110. package/templates/gitignore +8 -0
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Forges a rating can be collected on. The same two names as CI's [`Forge`],
3
+ * and deliberately a separate type: an index hosted on one forge could tally
4
+ * on the other, and nothing here needs them to move together.
5
+ */
6
+ export type RatingProviderKind = "github" | "gitlab";
7
+ export declare const RATING_PROVIDERS: readonly RatingProviderKind[];
8
+ /** Per-run thread-creation budget when the config names none. */
9
+ export declare const DEFAULT_CREATE_BUDGET = 400;
10
+ /**
11
+ * The `ratings` block, with every gap filled. The block being absent means
12
+ * ratings are off — that is the *absence* of this value, not a variant of it,
13
+ * so every reader gets `undefined` and has nothing to branch on twice.
14
+ */
15
+ export interface RatingsConfig {
16
+ /** Which forge holds the threads. Required — there is no sensible default. */
17
+ provider: RatingProviderKind;
18
+ /**
19
+ * Where threads live: a GitHub Discussions **category** name, or a GitLab
20
+ * **work item type**. Required, and no default is offered — "Ratings" is a
21
+ * category an operator creates, while GitLab's work item types are a closed
22
+ * set, so a default correct on one forge would be wrong on the other.
23
+ */
24
+ container: string;
25
+ /**
26
+ * Threads created per run. Default {@link DEFAULT_CREATE_BUDGET}, which sits
27
+ * under GitHub's 500-per-hour content-creation cap with room for the retries
28
+ * a run may spend. A partial run creates *fewer threads*, never corrupt
29
+ * state, so the only cost of a small budget is more runs to converge.
30
+ */
31
+ createBudget: number;
32
+ /**
33
+ * Lock every thread on creation. **Default `true`**: votes still count,
34
+ * replies are refused.
35
+ *
36
+ * Two things at once. It is the low-moderation default — an operator gets a
37
+ * rating signal without also running a comment forum they have to moderate.
38
+ * And it independently hardens R-1 clause 1: a locked thread cannot receive
39
+ * the forged-marker reply that clause exists to reject, so the marker rule
40
+ * and the lock have to fail together before a stranger's text is counted.
41
+ */
42
+ lockThreads: boolean;
43
+ }
44
+ /**
45
+ * Validate the `ratings` block of an already-parsed config object.
46
+ *
47
+ * Absent (or `null`) ⇒ `undefined` ⇒ ratings off. Unknown keys are ignored,
48
+ * the same forward-compatibility rule `stats.json` itself follows.
49
+ */
50
+ export declare function validateRatings(raw: unknown): RatingsConfig | undefined;
51
+ /**
52
+ * Read the `ratings` block out of `index.config.json`. A missing file, or a
53
+ * file with no `ratings` block, means ratings are off.
54
+ */
55
+ export declare function loadRatingsConfig(root: string): Promise<RatingsConfig | undefined>;
56
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/ratings/config.ts"],"names":[],"mappings":"AAiBA;;;;GAIG;AACH,MAAM,MAAM,kBAAkB,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAErD,eAAO,MAAM,gBAAgB,EAAE,SAAS,kBAAkB,EAAyB,CAAC;AAEpF,iEAAiE;AACjE,eAAO,MAAM,qBAAqB,MAAM,CAAC;AAEzC;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,8EAA8E;IAC9E,QAAQ,EAAE,kBAAkB,CAAC;IAC7B;;;;;OAKG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB;;;;;OAKG;IACH,YAAY,EAAE,MAAM,CAAC;IACrB;;;;;;;;;OASG;IACH,WAAW,EAAE,OAAO,CAAC;CACtB;AAMD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,GAAG,aAAa,GAAG,SAAS,CAgCvE;AAED;;;GAGG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CAkBxF"}
@@ -0,0 +1,86 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright 2026 The Grimoire Authors
3
+ // The `ratings` block of `index.config.json` — the third independent reader of
4
+ // that file, beside `config.ts` (the site) and `ci.ts` (the pipeline). One
5
+ // loader for all three would couple the renderer, the CI generator and the
6
+ // tally to each other's validation for the sake of one `readFile`; each stays
7
+ // able to fail on its own block and say so in its own words.
8
+ //
9
+ // There is deliberately no `botIds` key here. The author allowlist is
10
+ // `index-policy.json`'s `trustedBots[].id` and nothing else — a second copy of
11
+ // the same ids in a second file is a consistency hazard, not a convenience.
12
+ import fs from "node:fs/promises";
13
+ import path from "node:path";
14
+ import { CONFIG_FILE, SiteConfigError } from "../config.js";
15
+ export const RATING_PROVIDERS = ["github", "gitlab"];
16
+ /** Per-run thread-creation budget when the config names none. */
17
+ export const DEFAULT_CREATE_BUDGET = 400;
18
+ function fail(msg) {
19
+ throw new SiteConfigError(`${CONFIG_FILE}: ratings.${msg}`);
20
+ }
21
+ /**
22
+ * Validate the `ratings` block of an already-parsed config object.
23
+ *
24
+ * Absent (or `null`) ⇒ `undefined` ⇒ ratings off. Unknown keys are ignored,
25
+ * the same forward-compatibility rule `stats.json` itself follows.
26
+ */
27
+ export function validateRatings(raw) {
28
+ if (raw === undefined || raw === null)
29
+ return undefined;
30
+ if (typeof raw !== "object" || Array.isArray(raw)) {
31
+ throw new SiteConfigError(`${CONFIG_FILE}: ratings must be an object`);
32
+ }
33
+ const ratings = raw;
34
+ if (!RATING_PROVIDERS.includes(ratings.provider)) {
35
+ fail(`provider must be one of ${RATING_PROVIDERS.join(", ")}`);
36
+ }
37
+ if (typeof ratings.container !== "string" || ratings.container.trim() === "") {
38
+ fail("container must be a non-empty string - a GitHub Discussions category, or a GitLab work item type");
39
+ }
40
+ if (ratings.createBudget !== undefined) {
41
+ if (typeof ratings.createBudget !== "number" || !Number.isInteger(ratings.createBudget)) {
42
+ fail("createBudget must be a whole number");
43
+ }
44
+ if (ratings.createBudget < 0)
45
+ fail("createBudget must not be negative");
46
+ }
47
+ if (ratings.lockThreads !== undefined && typeof ratings.lockThreads !== "boolean") {
48
+ fail("lockThreads must be a boolean");
49
+ }
50
+ // Built key by key rather than spread: the returned value is the four
51
+ // documented keys and nothing else, so an unknown key is ignored in the
52
+ // strong sense — it cannot reach a consumer that happens to look for it.
53
+ return {
54
+ provider: ratings.provider,
55
+ container: ratings.container,
56
+ createBudget: ratings.createBudget ?? DEFAULT_CREATE_BUDGET,
57
+ lockThreads: ratings.lockThreads ?? true,
58
+ };
59
+ }
60
+ /**
61
+ * Read the `ratings` block out of `index.config.json`. A missing file, or a
62
+ * file with no `ratings` block, means ratings are off.
63
+ */
64
+ export async function loadRatingsConfig(root) {
65
+ let text;
66
+ try {
67
+ text = await fs.readFile(path.join(root, CONFIG_FILE), "utf8");
68
+ }
69
+ catch (err) {
70
+ if (err.code === "ENOENT")
71
+ return undefined;
72
+ throw err;
73
+ }
74
+ let parsed;
75
+ try {
76
+ parsed = JSON.parse(text);
77
+ }
78
+ catch (err) {
79
+ throw new SiteConfigError(`${CONFIG_FILE}: ${err.message}`);
80
+ }
81
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
82
+ throw new SiteConfigError(`${CONFIG_FILE}: must contain a JSON object`);
83
+ }
84
+ return validateRatings(parsed.ratings);
85
+ }
86
+ //# sourceMappingURL=config.js.map
@@ -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,16 @@
1
+ /**
2
+ * Nodes per page.
3
+ *
4
+ * 50 rather than the API maximum of 100. This used to be a workaround for
5
+ * `request()`'s fixed 1 MiB cap — a page carries every thread's full body,
6
+ * including bodies this bot did not write, so a container holding long human
7
+ * threads overran the cap and [`graphql`] reported it as a transport failure.
8
+ * That was a real failure on first runs, before any bot thread existed.
9
+ *
10
+ * The cap is now per-call (`LARGE_RESPONSE_BYTES`), so the size argument for
11
+ * keeping this at 50 is gone. It stays at 50 anyway: raising it changes request
12
+ * count and cursor behaviour for every existing index, which is a separate
13
+ * decision from fixing the overrun.
14
+ */
15
+ export declare const PAGE_SIZE = 50;
16
+ //# sourceMappingURL=paging.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paging.d.ts","sourceRoot":"","sources":["../../src/ratings/paging.ts"],"names":[],"mappings":"AAsBA;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,SAAS,KAAK,CAAC"}
@@ -0,0 +1,36 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright 2026 The Grimoire Authors
3
+ // Page size, alone in a module because of *where* it is read.
4
+ //
5
+ // `provider.ts` and the two implementations form a deliberate cycle: the
6
+ // factory imports both providers, and both import the shared plumbing back.
7
+ // `provider.ts` states the rule that makes that safe -- everything crossing the
8
+ // cycle is a hoisted function declaration, referenced only from inside a
9
+ // closure -- and a `const` interpolated into a module-level query string is
10
+ // precisely the case the rule excludes.
11
+ //
12
+ // It was declared in `provider.ts` and read at module scope by both providers,
13
+ // which the factory imports before it reaches the declaration. Under Node's ESM
14
+ // that is a temporal dead zone and every `ratings` run died on import; under
15
+ // Vitest's SSR transform the same read is a property access on a
16
+ // not-yet-populated namespace, so it quietly yielded `undefined` and shipped
17
+ // `first:undefined` in the query while 24 provider tests passed.
18
+ //
19
+ // Living here, the constant does not cross the cycle at all, so the rule holds
20
+ // by construction instead of by remembering it.
21
+ /**
22
+ * Nodes per page.
23
+ *
24
+ * 50 rather than the API maximum of 100. This used to be a workaround for
25
+ * `request()`'s fixed 1 MiB cap — a page carries every thread's full body,
26
+ * including bodies this bot did not write, so a container holding long human
27
+ * threads overran the cap and [`graphql`] reported it as a transport failure.
28
+ * That was a real failure on first runs, before any bot thread existed.
29
+ *
30
+ * The cap is now per-call (`LARGE_RESPONSE_BYTES`), so the size argument for
31
+ * keeping this at 50 is gone. It stays at 50 anyway: raising it changes request
32
+ * count and cursor behaviour for every existing index, which is a separate
33
+ * decision from fixing the overrun.
34
+ */
35
+ export const PAGE_SIZE = 50;
36
+ //# sourceMappingURL=paging.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paging.js","sourceRoot":"","sources":["../../src/ratings/paging.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,sCAAsC;AAEtC,8DAA8D;AAC9D,EAAE;AACF,yEAAyE;AACzE,4EAA4E;AAC5E,gFAAgF;AAChF,yEAAyE;AACzE,4EAA4E;AAC5E,wCAAwC;AACxC,EAAE;AACF,+EAA+E;AAC/E,gFAAgF;AAChF,6EAA6E;AAC7E,iEAAiE;AACjE,6EAA6E;AAC7E,iEAAiE;AACjE,EAAE;AACF,+EAA+E;AAC/E,gDAAgD;AAEhD;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,EAAE,CAAC"}
@@ -0,0 +1,100 @@
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
+ export { PAGE_SIZE } from "./paging.js";
15
+ /**
16
+ * A thread the reconcile loop may tally. Declared as an alias of R-1's
17
+ * `AuthorizedThread` rather than a second identical interface: R-1 is what
18
+ * produces these, and the type should say so.
19
+ */
20
+ export type RatingThread = AuthorizedThread;
21
+ export interface RatingProviderConfig {
22
+ /** GraphQL endpoint, e.g. `https://api.github.com/graphql`. */
23
+ api: string;
24
+ /** Credential with write access to threads. Never logged. */
25
+ token: string;
26
+ /** GitHub `owner/repo`; GitLab full project path. */
27
+ project: string;
28
+ /** GitHub Discussions category name; GitLab work item type name. */
29
+ container: string;
30
+ /** Lock threads on creation — votes count, replies are refused. */
31
+ lockThreads: boolean;
32
+ /** `index-policy.json`'s allowlist. R-1 clause 2 reads ids out of it. */
33
+ trustedBots?: readonly TrustedBot[];
34
+ }
35
+ export interface RatingProvider {
36
+ /**
37
+ * Every thread the bot authored in the configured container, paginated to
38
+ * exhaustion. R-1 filtering is applied **here**, never by the caller.
39
+ *
40
+ * A secondary rate limit part-way through throws [`RateLimited`] carrying the
41
+ * pages already read — a truncated pass is partial, not empty, and the caller
42
+ * needs both facts to publish without wiping what it could not observe.
43
+ */
44
+ listAuthored(): Promise<RatingThread[]>;
45
+ /** Create one thread carrying the marker. The budget is the caller's job. */
46
+ create(ref: string): Promise<RatingThread>;
47
+ }
48
+ /**
49
+ * The forge answered, and the answer was unusable: a transport failure, a
50
+ * non-2xx, a body that did not parse, or a 200 carrying GraphQL `errors`.
51
+ *
52
+ * A `CliError` subclass rather than a name in `classify()`'s list, following
53
+ * `seed.ts`: that list exists for errors thrown by modules `main.ts` must not
54
+ * import, and `cli/exit.js` is already one of its static imports — so the
55
+ * mapping here is by type, which cannot drift out of sync with a string.
56
+ */
57
+ export declare class ForgeError extends CliError {
58
+ constructor(message: string, code?: ExitCode);
59
+ }
60
+ /**
61
+ * A secondary rate limit. Distinguished from [`ForgeError`] because the two are
62
+ * handled oppositely: a forge error fails the run, a rate limit stops it early
63
+ * and publishes what it already got.
64
+ */
65
+ export declare class RateLimited extends ForgeError {
66
+ /** What the forge asked us to wait, by GitHub's documented precedence. */
67
+ readonly retryAfterMs: number;
68
+ /** Threads read before the limit. Empty on the create path. */
69
+ readonly observed: readonly RatingThread[];
70
+ constructor(message: string, retryAfterMs: number, observed?: readonly RatingThread[]);
71
+ }
72
+ /** GitHub's documented floor when neither header says how long to wait. */
73
+ export declare const BACKOFF_FLOOR_MS = 60000;
74
+ /**
75
+ * The body every provider writes. The marker sits on its own line, which is the
76
+ * only position R-1's anchored parser accepts; the prose above it is for the
77
+ * human who lands on the thread from the catalog.
78
+ */
79
+ export declare function threadBody(ref: string): string;
80
+ export declare function field(data: unknown, key: string): unknown;
81
+ /** `field`, chained. Every GraphQL read here is a path into an `unknown`. */
82
+ export declare function at(data: unknown, ...keys: string[]): unknown;
83
+ export declare function stringAt(data: unknown, ...keys: string[]): string;
84
+ export declare function nodes(data: unknown): unknown[];
85
+ /**
86
+ * One GraphQL round trip, with the branching every provider shares.
87
+ *
88
+ * Order matters and is the reason this is one function: a rate limit is
89
+ * classified first (it is the one failure that must not fail the run), then
90
+ * `errors` is checked **independently of the HTTP status and before `data` is
91
+ * touched** — a 200 with `errors` and a null `data` otherwise reads as
92
+ * "genuinely fewer votes observed", which is silent emptying arriving through
93
+ * the one door R-2 does not watch.
94
+ *
95
+ * @param observed Threads already read, attached to a [`RateLimited`] so a
96
+ * truncated pagination pass stays partial rather than becoming empty.
97
+ */
98
+ export declare function graphql(api: string, headers: Record<string, string>, query: string, variables: Record<string, unknown>, observed?: readonly RatingThread[]): Promise<Record<string, unknown>>;
99
+ export declare function createRatingProvider(kind: "github" | "gitlab", config: RatingProviderConfig): RatingProvider;
100
+ //# 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;AAQhE,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAIxC;;;;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;AAGvC;;;;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,CAmDlC;AAED,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,QAAQ,GAAG,QAAQ,EACzB,MAAM,EAAE,oBAAoB,GAC3B,cAAc,CAIhB"}