diffninja 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +259 -0
- package/dist/calltree.d.ts +47 -0
- package/dist/calltree.js +296 -0
- package/dist/cli.d.ts +57 -0
- package/dist/cli.js +340 -0
- package/dist/diff.d.ts +7 -0
- package/dist/diff.js +114 -0
- package/dist/extract.d.ts +26 -0
- package/dist/extract.js +152 -0
- package/dist/git.d.ts +40 -0
- package/dist/git.js +288 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +8 -0
- package/dist/infer.d.ts +21 -0
- package/dist/infer.js +189 -0
- package/dist/languages/bash.d.ts +2 -0
- package/dist/languages/bash.js +208 -0
- package/dist/languages/c.d.ts +2 -0
- package/dist/languages/c.js +218 -0
- package/dist/languages/call-syntax.d.ts +125 -0
- package/dist/languages/call-syntax.js +997 -0
- package/dist/languages/cpp.d.ts +2 -0
- package/dist/languages/cpp.js +321 -0
- package/dist/languages/csharp.d.ts +2 -0
- package/dist/languages/csharp.js +324 -0
- package/dist/languages/elixir.d.ts +2 -0
- package/dist/languages/elixir.js +331 -0
- package/dist/languages/go.d.ts +2 -0
- package/dist/languages/go.js +299 -0
- package/dist/languages/grammars.d.ts +50 -0
- package/dist/languages/grammars.js +351 -0
- package/dist/languages/haskell.d.ts +2 -0
- package/dist/languages/haskell.js +250 -0
- package/dist/languages/java.d.ts +2 -0
- package/dist/languages/java.js +351 -0
- package/dist/languages/javascript.d.ts +4 -0
- package/dist/languages/javascript.js +648 -0
- package/dist/languages/kotlin.d.ts +2 -0
- package/dist/languages/kotlin.js +368 -0
- package/dist/languages/lua.d.ts +2 -0
- package/dist/languages/lua.js +212 -0
- package/dist/languages/ocaml.d.ts +2 -0
- package/dist/languages/ocaml.js +291 -0
- package/dist/languages/perl.d.ts +2 -0
- package/dist/languages/perl.js +418 -0
- package/dist/languages/php.d.ts +2 -0
- package/dist/languages/php.js +397 -0
- package/dist/languages/python.d.ts +2 -0
- package/dist/languages/python.js +376 -0
- package/dist/languages/registry.d.ts +7 -0
- package/dist/languages/registry.js +69 -0
- package/dist/languages/ruby.d.ts +2 -0
- package/dist/languages/ruby.js +391 -0
- package/dist/languages/rust.d.ts +2 -0
- package/dist/languages/rust.js +261 -0
- package/dist/languages/scala.d.ts +2 -0
- package/dist/languages/scala.js +307 -0
- package/dist/languages/solidity.d.ts +2 -0
- package/dist/languages/solidity.js +240 -0
- package/dist/languages/swift.d.ts +2 -0
- package/dist/languages/swift.js +268 -0
- package/dist/languages/types.d.ts +36 -0
- package/dist/languages/types.js +74 -0
- package/dist/languages/typescript-contracts.d.ts +57 -0
- package/dist/languages/typescript-contracts.js +528 -0
- package/dist/languages/typescript-dispatch.d.ts +68 -0
- package/dist/languages/typescript-dispatch.js +710 -0
- package/dist/languages/typescript.d.ts +4 -0
- package/dist/languages/typescript.js +722 -0
- package/dist/languages/zig.d.ts +2 -0
- package/dist/languages/zig.js +243 -0
- package/dist/loc.d.ts +17 -0
- package/dist/loc.js +34 -0
- package/dist/reach.d.ts +17 -0
- package/dist/reach.js +65 -0
- package/dist/render.d.ts +18 -0
- package/dist/render.js +83 -0
- package/dist/review/brand.d.ts +8 -0
- package/dist/review/brand.js +25 -0
- package/dist/review/call-context.d.ts +27 -0
- package/dist/review/call-context.js +446 -0
- package/dist/review/call-flow-html.d.ts +32 -0
- package/dist/review/call-flow-html.js +1870 -0
- package/dist/review/call-flow-nav.d.ts +151 -0
- package/dist/review/call-flow-nav.js +317 -0
- package/dist/review/call-flow.d.ts +47 -0
- package/dist/review/call-flow.js +229 -0
- package/dist/review/change-facts.d.ts +69 -0
- package/dist/review/change-facts.js +729 -0
- package/dist/review/cli.d.ts +2 -0
- package/dist/review/cli.js +50 -0
- package/dist/review/connected-analysis.d.ts +100 -0
- package/dist/review/connected-analysis.js +163 -0
- package/dist/review/connected-html.d.ts +17 -0
- package/dist/review/connected-html.js +2853 -0
- package/dist/review/connected.d.ts +23 -0
- package/dist/review/connected.js +141 -0
- package/dist/review/escape-html.d.ts +2 -0
- package/dist/review/escape-html.js +9 -0
- package/dist/review/evidence-html.d.ts +21 -0
- package/dist/review/evidence-html.js +521 -0
- package/dist/review/evidence-syntax.d.ts +132 -0
- package/dist/review/evidence-syntax.js +478 -0
- package/dist/review/evidence-types.d.ts +62 -0
- package/dist/review/evidence-types.js +1 -0
- package/dist/review/evidence.d.ts +31 -0
- package/dist/review/evidence.js +1603 -0
- package/dist/review/file-role.d.ts +9 -0
- package/dist/review/file-role.js +29 -0
- package/dist/review/github.d.ts +204 -0
- package/dist/review/github.js +1245 -0
- package/dist/review/history.d.ts +101 -0
- package/dist/review/history.js +412 -0
- package/dist/review/html.d.ts +34 -0
- package/dist/review/html.js +1104 -0
- package/dist/review/input.d.ts +10 -0
- package/dist/review/input.js +113 -0
- package/dist/review/intent.d.ts +4 -0
- package/dist/review/intent.js +75 -0
- package/dist/review/mcp-cli.d.ts +2 -0
- package/dist/review/mcp-cli.js +25 -0
- package/dist/review/mcp.d.ts +12 -0
- package/dist/review/mcp.js +414 -0
- package/dist/review/module-resolution.d.ts +2 -0
- package/dist/review/module-resolution.js +86 -0
- package/dist/review/palette.d.ts +7 -0
- package/dist/review/palette.js +104 -0
- package/dist/review/pipeline.d.ts +77 -0
- package/dist/review/pipeline.js +227 -0
- package/dist/review/pr-input.d.ts +19 -0
- package/dist/review/pr-input.js +130 -0
- package/dist/review/questions.d.ts +201 -0
- package/dist/review/questions.js +174 -0
- package/dist/review/reference-check.d.ts +7 -0
- package/dist/review/reference-check.js +733 -0
- package/dist/review/report-pages.d.ts +109 -0
- package/dist/review/report-pages.js +328 -0
- package/dist/review/service.d.ts +23 -0
- package/dist/review/service.js +198 -0
- package/dist/review/setup.d.ts +112 -0
- package/dist/review/setup.js +549 -0
- package/dist/review/source.d.ts +26 -0
- package/dist/review/source.js +276 -0
- package/dist/review/toml.d.ts +38 -0
- package/dist/review/toml.js +565 -0
- package/dist/review/types.d.ts +179 -0
- package/dist/review/types.js +1 -0
- package/dist/run.d.ts +49 -0
- package/dist/run.js +311 -0
- package/dist/types.d.ts +366 -0
- package/dist/types.js +83 -0
- package/package.json +88 -0
- package/scripts/ensure-native-grammar.mjs +188 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type Server } from "node:http";
|
|
2
|
+
import { ConnectedReview } from "./github.js";
|
|
3
|
+
import type { ConnectedAnalysisView } from "./connected-analysis.js";
|
|
4
|
+
export interface ConnectedSession {
|
|
5
|
+
server: Server;
|
|
6
|
+
url: string;
|
|
7
|
+
}
|
|
8
|
+
/** Read-only extras a connected session can serve beside the review itself. */
|
|
9
|
+
export interface ConnectedOptions {
|
|
10
|
+
/**
|
|
11
|
+
* The local analysis of the loaded snapshot, for `GET /api/analysis`. It must
|
|
12
|
+
* describe the snapshot the review currently holds, or say it is unavailable.
|
|
13
|
+
*/
|
|
14
|
+
readonly analysis?: () => Promise<ConnectedAnalysisView>;
|
|
15
|
+
/**
|
|
16
|
+
* The call-flow page of the analysis of snapshot `snapshotId`, for one changed
|
|
17
|
+
* file or all of them, for `GET /flow`. Undefined when that snapshot is not
|
|
18
|
+
* the one analyzed or it has no call flows there.
|
|
19
|
+
*/
|
|
20
|
+
readonly flow?: (snapshotId: string, file: string | undefined) => Promise<string | undefined>;
|
|
21
|
+
}
|
|
22
|
+
/** A single ephemeral session; never exposes a general GitHub API proxy. */
|
|
23
|
+
export declare function serveConnected(review?: ConnectedReview, options?: ConnectedOptions): Promise<ConnectedSession>;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { ConnectedReview } from "./github.js";
|
|
5
|
+
import { renderConnectedPage } from "./connected-html.js";
|
|
6
|
+
import { reportPolicy } from "./report-pages.js";
|
|
7
|
+
const MAX_BODY = 256 * 1024;
|
|
8
|
+
const loadSchema = z.object({ url: z.string() }).strict();
|
|
9
|
+
const reviewSchema = z.object({
|
|
10
|
+
snapshotId: z.string(), event: z.enum(["COMMENT", "APPROVE", "REQUEST_CHANGES"]), body: z.string(),
|
|
11
|
+
comments: z.array(z.object({ path: z.string(), line: z.number().int().positive(), side: z.enum(["LEFT", "RIGHT"]), body: z.string() }).strict()),
|
|
12
|
+
}).strict();
|
|
13
|
+
const emptySchema = z.object({}).strict();
|
|
14
|
+
const tokenSchema = z.string().regex(/^[a-f0-9]{64}$/);
|
|
15
|
+
const NO_ANALYSIS = {
|
|
16
|
+
available: false,
|
|
17
|
+
reason: "No local analysis is attached to this session.",
|
|
18
|
+
};
|
|
19
|
+
async function readBody(req) {
|
|
20
|
+
if (req.headers["content-type"] !== "application/json")
|
|
21
|
+
throw new Error("Expected application/json.");
|
|
22
|
+
const chunks = [];
|
|
23
|
+
let length = 0;
|
|
24
|
+
for await (const chunk of req) {
|
|
25
|
+
length += chunk.length;
|
|
26
|
+
if (length > MAX_BODY)
|
|
27
|
+
throw new Error("Request exceeds 256 KiB.");
|
|
28
|
+
chunks.push(chunk);
|
|
29
|
+
}
|
|
30
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Loopback-only guard. The request must name this session's exact host, must
|
|
34
|
+
* not carry a foreign `Origin`, and must not announce a cross-site fetch. A
|
|
35
|
+
* browser page on another origin can produce none of these headers honestly.
|
|
36
|
+
*/
|
|
37
|
+
function requestIsTrusted(req, origin) {
|
|
38
|
+
if (req.headers.host !== new URL(origin).host)
|
|
39
|
+
return false;
|
|
40
|
+
if (req.headers.origin !== undefined && req.headers.origin !== origin)
|
|
41
|
+
return false;
|
|
42
|
+
const site = req.headers["sec-fetch-site"];
|
|
43
|
+
return !site || ["same-origin", "none"].includes(String(site));
|
|
44
|
+
}
|
|
45
|
+
/** A single ephemeral session; never exposes a general GitHub API proxy. */
|
|
46
|
+
export async function serveConnected(review = new ConnectedReview(), options = {}) {
|
|
47
|
+
const csrf = randomBytes(32).toString("hex");
|
|
48
|
+
let origin = "";
|
|
49
|
+
const server = createServer(async (req, res) => {
|
|
50
|
+
res.setHeader("Cache-Control", "no-store");
|
|
51
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
52
|
+
res.setHeader("X-Frame-Options", "DENY");
|
|
53
|
+
res.setHeader("Referrer-Policy", "no-referrer");
|
|
54
|
+
res.setHeader("Content-Security-Policy", `default-src 'none'; script-src 'nonce-${csrf}'; style-src 'nonce-${csrf}'; connect-src 'self'; frame-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'`);
|
|
55
|
+
const json = (code, value) => { res.writeHead(code, { "Content-Type": "application/json" }); res.end(JSON.stringify(value)); };
|
|
56
|
+
if (!requestIsTrusted(req, origin)) {
|
|
57
|
+
json(403, { error: "Untrusted Host or Origin." });
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (req.method === "GET" && req.url === "/") {
|
|
61
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
62
|
+
res.end(renderConnectedPage(csrf));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (req.method === "GET" && req.url === "/api/state") {
|
|
66
|
+
json(200, review.getState());
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (req.method === "GET" && req.url === "/api/analysis") {
|
|
70
|
+
try {
|
|
71
|
+
json(200, options.analysis === undefined ? NO_ANALYSIS : await options.analysis());
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
json(200, { available: false, reason: `Local analysis failed: ${error instanceof Error ? error.message : "unknown error"}` });
|
|
75
|
+
}
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (req.method === "GET" && (req.url ?? "").startsWith("/flow?")) {
|
|
79
|
+
const query = new URL(req.url ?? "", origin).searchParams;
|
|
80
|
+
const snapshotId = query.get("snapshot") ?? "";
|
|
81
|
+
const file = query.get("file") ?? undefined;
|
|
82
|
+
let html;
|
|
83
|
+
try {
|
|
84
|
+
html = options.flow === undefined ? undefined : await options.flow(snapshotId, file);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
html = undefined;
|
|
88
|
+
}
|
|
89
|
+
if (html === undefined) {
|
|
90
|
+
json(404, { error: "No call flow for that revision and file." });
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
// Only this page may frame it: the drawer beside the diff.
|
|
94
|
+
res.setHeader("Content-Security-Policy", reportPolicy(html).replace("frame-ancestors 'none'", "frame-ancestors 'self'"));
|
|
95
|
+
res.setHeader("X-Frame-Options", "SAMEORIGIN");
|
|
96
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
97
|
+
res.end(html);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const routes = ["/api/load", "/api/preview", "/api/submit", "/api/reconcile"];
|
|
101
|
+
if (req.method !== "POST" || !routes.includes(req.url ?? "")) {
|
|
102
|
+
json(404, { error: "Not found." });
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const token = tokenSchema.safeParse(req.headers["x-diffninja-csrf"]);
|
|
106
|
+
if (req.headers.origin !== origin || !token.success || !timingSafeEqual(Buffer.from(token.data), Buffer.from(csrf))) {
|
|
107
|
+
json(403, { error: "Invalid session or CSRF token." });
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
const text = await readBody(req);
|
|
112
|
+
let result;
|
|
113
|
+
switch (req.url) {
|
|
114
|
+
case "/api/load":
|
|
115
|
+
result = await review.load(loadSchema.parse(JSON.parse(text)).url);
|
|
116
|
+
break;
|
|
117
|
+
case "/api/preview":
|
|
118
|
+
result = await review.preview(reviewSchema.parse(JSON.parse(text)));
|
|
119
|
+
break;
|
|
120
|
+
case "/api/submit":
|
|
121
|
+
result = await review.submit(reviewSchema.parse(JSON.parse(text)));
|
|
122
|
+
break;
|
|
123
|
+
default:
|
|
124
|
+
emptySchema.parse(JSON.parse(text));
|
|
125
|
+
result = await review.reconcile();
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
json(200, result);
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
const message = error instanceof z.ZodError ? "Invalid request fields: " + error.issues.map(issue => issue.path.join(".") || "input").join(", ") : error instanceof Error ? error.message : "Request failed.";
|
|
132
|
+
json(400, { error: message, state: review.getState() });
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
server.requestTimeout = 15_000;
|
|
136
|
+
server.headersTimeout = 10_000;
|
|
137
|
+
await new Promise((resolve, reject) => { server.once("error", reject); server.listen(0, "127.0.0.1", () => { server.removeListener("error", reject); resolve(); }); });
|
|
138
|
+
const address = z.object({ port: z.number().int().positive() }).parse(server.address());
|
|
139
|
+
origin = `http://127.0.0.1:${address.port}`;
|
|
140
|
+
return { server, url: origin + "/" };
|
|
141
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Escape untrusted text for HTML element and attribute contexts. */
|
|
2
|
+
export function escapeHtml(text) {
|
|
3
|
+
return text
|
|
4
|
+
.replaceAll("&", "&")
|
|
5
|
+
.replaceAll("<", "<")
|
|
6
|
+
.replaceAll(">", ">")
|
|
7
|
+
.replaceAll('"', """)
|
|
8
|
+
.replaceAll("'", "'");
|
|
9
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ReviewReport } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* The opening view of a review report: what the pull request says it does, what
|
|
4
|
+
* the gathered evidence actually established, the short reading agenda, and the
|
|
5
|
+
* automatic findings with the source behind them.
|
|
6
|
+
*
|
|
7
|
+
* Every string is a fixed template or escaped report data. The pull request
|
|
8
|
+
* title, description and evidence text are untrusted input: they are escaped
|
|
9
|
+
* into text nodes and never reach the page script. No numeric priority, model
|
|
10
|
+
* probability or judgment value is printed here, and no check is described as
|
|
11
|
+
* passing: a check states the scope it ran over and where its certainty ends,
|
|
12
|
+
* and a finding carries its own limitation.
|
|
13
|
+
*
|
|
14
|
+
* The order is the reading order: outcome first, then the checks that ran and
|
|
15
|
+
* what they did not cover, then the agenda, then the findings the agenda points
|
|
16
|
+
* at. One piece of evidence is rendered once — an excerpt a finding already
|
|
17
|
+
* carries is linked from the agenda instead of repeated, and a definition cited
|
|
18
|
+
* by two agenda entries is shown in the first and linked from the second.
|
|
19
|
+
*/
|
|
20
|
+
export declare function renderBrief(report: ReviewReport): string;
|
|
21
|
+
export declare const BRIEF_STYLES = "\n.brief { display: flex; flex-direction: column; gap: 7px; }\n.brief > h2 { font-size: 13.5px; text-transform: uppercase; letter-spacing: 0.1em; color: var(--accent); margin-top: 7px; }\n.brief > h2:first-child { margin-top: 0; }\n.brief h3 { font-size: 13px; color: var(--ink-soft); font-weight: 600; }\n.pr-facts { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 4px 12px; margin: 0; }\n.pr-facts dt { font-size: 12px; color: var(--ink-soft); }\n.pr-facts dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }\n.pr-title { font-size: 17px; font-weight: 700; }\n.pr-body > summary { cursor: pointer; font-size: 12.5px; color: var(--ink-soft); }\n.pr-text {\n margin: 6px 0 0;\n padding: 9px 11px;\n border: 1px solid var(--line);\n border-radius: 6px;\n background: var(--sunken);\n font-family: var(--mono);\n font-size: 12.5px;\n line-height: 1.5;\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n max-height: 40vh;\n overflow: auto;\n}\n.intent { border: 1px solid var(--line); border-left: 5px solid var(--line-strong); border-radius: 6px; padding: 10px 14px; background: var(--panel); }\n.intent-done { border-left-color: var(--teal); }\n.intent-warn { border-left-color: var(--warn); }\n.intent-summary { font-size: 14.5px; overflow-wrap: anywhere; }\n.intent-verdict { margin-bottom: 4px; }\n.ev-badge {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n border: 1px solid var(--line-strong);\n border-radius: 999px;\n padding: 2px 10px;\n font-size: 11.5px;\n font-weight: 600;\n white-space: nowrap;\n}\n.ev-badge-done { border-color: var(--teal); color: var(--teal); background: var(--teal-bg); }\n.ev-badge-warn { border-color: var(--warn); color: var(--warn); background: var(--warn-bg); }\n.claims, .checks, .findings, .evidence { list-style: none; margin: 5px 0 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }\n.claim, .check, .finding, .ev-card { border: 1px solid var(--line); border-radius: 6px; padding: 6px 9px; background: var(--panel); }\n.claim-head, .check-head, .finding-head, .ev-head, .ag-head { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }\nsummary.ev-head, summary.check-head { cursor: pointer; }\nsummary.ev-head::before, summary.check-head::before { content: \"+\"; font-family: var(--mono); }\ndetails[open] > summary.ev-head::before, details[open] > summary.check-head::before { content: \"-\"; font-family: var(--mono); }\n.claim-origin, .ev-role, .finding-kind, .check-kind { font-size: 11.5px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; color: var(--ink-soft); }\n.claim-text, .ag-title, .finding-title { font-size: 14.5px; font-weight: 600; overflow-wrap: anywhere; }\n.claim-note, .ag-reason { font-size: 13px; color: var(--ink-soft); overflow-wrap: anywhere; }\n.check-detail, .finding-scope, .finding-limit, .ev-label, .ev-note, .ag-links, .finding-hunks, .claim-links {\n font-size: 12.5px;\n color: var(--ink-soft);\n overflow-wrap: anywhere;\n margin-top: 3px;\n}\n.finding-label { font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; font-size: 11px; }\n.ev-where, .ev-ref { font-size: 12px; color: var(--ink-soft); overflow-wrap: anywhere; }\n.ev-card { background: var(--sunken); }\n.ev-code {\n margin: 6px 0 0;\n padding: 7px 9px;\n border: 1px solid var(--line);\n border-radius: 6px;\n background: var(--panel);\n font-family: var(--mono);\n font-size: 12px;\n line-height: 1.5;\n max-height: 26vh;\n overflow: auto;\n tab-size: 4;\n}\n.ev-hunk { font-size: 12px; white-space: nowrap; }\n.agenda { list-style: none; counter-reset: agenda; margin: 6px 0 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }\n.ag-card { border: 1px solid var(--line); border-left: 5px solid var(--line-strong); border-radius: 6px; padding: 9px 12px; background: var(--panel); }\n.ag-rank {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-width: 20px;\n height: 20px;\n border-radius: 999px;\n background: var(--sunken);\n border: 1px solid var(--line);\n font-size: 11.5px;\n font-weight: 700;\n color: var(--ink-soft);\n}\n.ag-ctx-list { display: flex; flex-direction: column; gap: 4px; margin-top: 6px; }\n.ag-ctx > summary { cursor: pointer; font-size: 12.5px; color: var(--ink-soft); overflow-wrap: anywhere; }\n.ag-ctx { border: 1px dashed var(--line-strong); border-radius: 6px; padding: 6px 9px; }\n.ag-ctx-again { font-size: 12.5px; }\n.obligations { margin-top: 8px; }\n.obligations > summary { cursor: pointer; font-size: 12.5px; color: var(--ink-soft); }\n.obligations ul { margin: 6px 0 0; padding-left: 20px; font-size: 13px; }\n.obligations li { overflow-wrap: anywhere; }\n@media (max-width: 680px) {\n .pr-facts { grid-template-columns: minmax(0, 1fr); gap: 0 0; }\n .pr-facts dd { margin-bottom: 6px; }\n}\n";
|