@riddledc/riddle-proof 0.8.37 → 0.8.39

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 CHANGED
@@ -968,6 +968,27 @@ way to a ready PR after proof and CI; `leave_draft: true` is only an explicit
968
968
  debug or user-request escape hatch. The notification adapter is where a host
969
969
  updates Discord, OpenClaw, GitHub, or another integration.
970
970
 
971
+ ## PR Proof Comments
972
+
973
+ Use `pr-comment` when a project already has a PR and a Riddle Proof output
974
+ directory, but is not using the full ship loop. It reads
975
+ `riddle-run-response.json` and `result.json`, builds a managed GitHub comment,
976
+ embeds the primary hosted screenshot when available, links structured artifacts,
977
+ summarizes check counts, and updates the existing Riddle Proof comment on
978
+ reruns.
979
+
980
+ ```sh
981
+ riddle-proof-loop pr-comment \
982
+ --proof-dir test-results/riddle-proof-docs \
983
+ --pr 228 \
984
+ --repo davisdiehl/riddle-site
985
+ ```
986
+
987
+ Use `--dry-run` to print the exact Markdown without touching GitHub, or
988
+ `--body-file proof-comment.md` to save the generated body for another tool. The
989
+ markdown builder is also exported from `@riddledc/riddle-proof/pr-comment` for
990
+ host integrations that want to post comments themselves.
991
+
971
992
  ## Runtime Scratch Space
972
993
 
973
994
  The packaged proof-run setup uses isolated git worktrees for before and after
@@ -0,0 +1,209 @@
1
+ // src/pr-comment.ts
2
+ var RIDDLE_PROOF_PR_COMMENT_MARKER = "<!-- riddle-proof:pr-comment:v1 -->";
3
+ function asRecord(value) {
4
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
5
+ }
6
+ function asArray(value) {
7
+ return Array.isArray(value) ? value : [];
8
+ }
9
+ function stringValue(value) {
10
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
11
+ }
12
+ function numberValue(value) {
13
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
14
+ }
15
+ function booleanValue(value) {
16
+ return typeof value === "boolean" ? value : void 0;
17
+ }
18
+ function artifactKind(name, url) {
19
+ const target = `${name} ${url}`.toLowerCase();
20
+ if (/\.(png|jpe?g|gif|webp|avif|svg)(\?|#|$)/.test(target)) return "image";
21
+ if (/\.(json|har|txt|md|html|log)(\?|#|$)/.test(target)) return "data";
22
+ return "artifact";
23
+ }
24
+ function artifactDisplayName(value, fallback) {
25
+ const raw = stringValue(value);
26
+ if (raw) return raw;
27
+ return fallback;
28
+ }
29
+ function collectArtifacts(runResponse) {
30
+ const proofResult = asRecord(runResponse.proofResult);
31
+ const outputs = asArray(proofResult.outputs);
32
+ const artifacts = [];
33
+ const seen = /* @__PURE__ */ new Set();
34
+ for (const [index, item] of outputs.entries()) {
35
+ const artifact = asRecord(item);
36
+ const url = stringValue(artifact.url);
37
+ if (!url || seen.has(url)) continue;
38
+ seen.add(url);
39
+ const name = artifactDisplayName(artifact.name, `artifact-${index + 1}`);
40
+ artifacts.push({
41
+ name,
42
+ url,
43
+ kind: artifactKind(name, url),
44
+ size_bytes: numberValue(artifact.size)
45
+ });
46
+ }
47
+ return artifacts;
48
+ }
49
+ function pageSummaries(result) {
50
+ const pages = [];
51
+ for (const page of asArray(result.pages)) {
52
+ const record = asRecord(page);
53
+ const route = stringValue(record.route) || stringValue(record.url) || "page";
54
+ const checks = asRecord(record.checks);
55
+ let passed = 0;
56
+ let failed = 0;
57
+ for (const value of Object.values(checks)) {
58
+ if (value === true) passed += 1;
59
+ if (value === false) failed += 1;
60
+ }
61
+ pages.push({ route, passed, failed });
62
+ }
63
+ return pages;
64
+ }
65
+ function summarizeExplicitChecks(value) {
66
+ let passed = 0;
67
+ let failed = 0;
68
+ const visit = (current, inChecks = false) => {
69
+ if (current === true && inChecks) {
70
+ passed += 1;
71
+ return;
72
+ }
73
+ if (current === false && inChecks) {
74
+ failed += 1;
75
+ return;
76
+ }
77
+ if (Array.isArray(current)) {
78
+ for (const item of current) visit(item, inChecks);
79
+ return;
80
+ }
81
+ if (current && typeof current === "object") {
82
+ for (const [key, item] of Object.entries(current)) {
83
+ visit(item, inChecks || key === "checks");
84
+ }
85
+ }
86
+ };
87
+ visit(value);
88
+ return { passed, failed };
89
+ }
90
+ function selectPrimaryImage(artifacts) {
91
+ const images = artifacts.filter((artifact) => artifact.kind === "image");
92
+ return images.find((artifact) => /after|proof|screenshot/i.test(artifact.name)) || images[0];
93
+ }
94
+ function summarizeRiddleProofPrComment(input) {
95
+ const runResponse = asRecord(input.runResponse);
96
+ const result = asRecord(input.result);
97
+ const proofResult = asRecord(runResponse.proofResult);
98
+ const preview = asRecord(runResponse.preview);
99
+ const artifacts = collectArtifacts(runResponse);
100
+ const pages = pageSummaries(result);
101
+ const checkSource = { ...result };
102
+ delete checkSource.ok;
103
+ const nestedChecks = summarizeExplicitChecks(checkSource);
104
+ const ok = booleanValue(result.ok) ?? booleanValue(runResponse.ok) ?? null;
105
+ return {
106
+ ok,
107
+ status: stringValue(proofResult.status),
108
+ job_id: stringValue(proofResult.job_id),
109
+ duration_ms: numberValue(proofResult.duration_ms),
110
+ proof_url: stringValue(runResponse.proofUrl),
111
+ preview_id: stringValue(preview.id),
112
+ preview_url: stringValue(preview.preview_url) || stringValue(preview.url),
113
+ preview_publish_recovered: booleanValue(preview.publish_recovered),
114
+ preview_publish_error: stringValue(preview.publish_error),
115
+ passed_checks: nestedChecks.passed,
116
+ failed_checks: nestedChecks.failed,
117
+ pages,
118
+ artifacts,
119
+ primary_image: selectPrimaryImage(artifacts)
120
+ };
121
+ }
122
+ function formatDuration(ms) {
123
+ if (typeof ms !== "number" || !Number.isFinite(ms)) return "";
124
+ const seconds = Math.max(0, Math.round(ms / 1e3));
125
+ const minutes = Math.floor(seconds / 60);
126
+ const remainder = seconds % 60;
127
+ return minutes > 0 ? `${minutes}m${String(remainder).padStart(2, "0")}s` : `${seconds}s`;
128
+ }
129
+ function markdownLink(label, url) {
130
+ return `[${label.replace(/\]/g, "\\]")}](${url})`;
131
+ }
132
+ function resultLabel(summary) {
133
+ if (summary.ok === true) return "passed";
134
+ if (summary.ok === false) return "failed";
135
+ return summary.status || "recorded";
136
+ }
137
+ function artifactRank(artifact) {
138
+ const name = artifact.name.toLowerCase();
139
+ if (name === "proof.json") return 0;
140
+ if (name === "result.json") return 1;
141
+ if (name.includes("proof") && name.endsWith(".json") && !name.includes("layout")) return 2;
142
+ if (name === "console.json") return 3;
143
+ if (artifact.kind === "data") return 10;
144
+ if (artifact.kind === "image") return 20;
145
+ return 30;
146
+ }
147
+ function buildRiddleProofPrCommentMarkdown(input) {
148
+ const summary = summarizeRiddleProofPrComment(input);
149
+ const title = input.title?.trim() || "Riddle Proof Evidence";
150
+ const lines = [
151
+ RIDDLE_PROOF_PR_COMMENT_MARKER,
152
+ `## ${title}`,
153
+ "",
154
+ `**Result:** ${resultLabel(summary)}`
155
+ ];
156
+ if (input.goal?.trim()) lines.push(`**Goal:** ${input.goal.trim()}`);
157
+ if (input.successCriteria?.trim()) lines.push(`**Success criteria:** ${input.successCriteria.trim()}`);
158
+ if (summary.status) lines.push(`**Riddle job status:** ${summary.status}`);
159
+ if (summary.job_id) lines.push(`**Riddle job:** \`${summary.job_id}\``);
160
+ if (summary.duration_ms) lines.push(`**Duration:** ${formatDuration(summary.duration_ms)}`);
161
+ if (summary.proof_url) lines.push(`**Proof URL:** ${markdownLink(summary.proof_url, summary.proof_url)}`);
162
+ if (summary.preview_id || summary.preview_url) {
163
+ const previewLabel = summary.preview_id ? `\`${summary.preview_id}\`` : "preview";
164
+ lines.push(`**Preview:** ${summary.preview_url ? markdownLink(previewLabel, summary.preview_url) : previewLabel}`);
165
+ }
166
+ if (summary.preview_publish_recovered) {
167
+ const detail = summary.preview_publish_error ? `: ${summary.preview_publish_error}` : "";
168
+ lines.push(`**Preview publish recovery:** recovered after publish error${detail}`);
169
+ }
170
+ lines.push(`**Checks:** ${summary.passed_checks} passed / ${summary.failed_checks} failed`);
171
+ lines.push("");
172
+ if (summary.primary_image) {
173
+ lines.push("### Screenshot");
174
+ lines.push(`![${summary.primary_image.name}](${summary.primary_image.url})`);
175
+ lines.push("");
176
+ }
177
+ if (summary.pages.length) {
178
+ lines.push("### Page Checks");
179
+ for (const page of summary.pages.slice(0, 12)) {
180
+ lines.push(`- \`${page.route}\`: ${page.passed} passed / ${page.failed} failed`);
181
+ }
182
+ if (summary.pages.length > 12) lines.push(`- ${summary.pages.length - 12} more page(s) omitted`);
183
+ lines.push("");
184
+ }
185
+ const linkedArtifacts = summary.artifacts.filter((artifact) => artifact.url !== summary.primary_image?.url).sort((left, right) => artifactRank(left) - artifactRank(right) || left.name.localeCompare(right.name)).slice(0, 20);
186
+ if (linkedArtifacts.length) {
187
+ lines.push("### Artifacts");
188
+ for (const artifact of linkedArtifacts) {
189
+ lines.push(`- ${markdownLink(artifact.name, artifact.url)}`);
190
+ }
191
+ if (summary.artifacts.length - (summary.primary_image ? 1 : 0) > linkedArtifacts.length) {
192
+ lines.push(`- ${summary.artifacts.length - (summary.primary_image ? 1 : 0) - linkedArtifacts.length} more artifact(s) omitted`);
193
+ }
194
+ lines.push("");
195
+ }
196
+ if (input.source?.trim()) {
197
+ lines.push(`_Source: ${input.source.trim()}_`);
198
+ } else {
199
+ lines.push("_Updated by `riddle-proof-loop pr-comment`._");
200
+ }
201
+ return `${lines.join("\n").trim()}
202
+ `;
203
+ }
204
+
205
+ export {
206
+ RIDDLE_PROOF_PR_COMMENT_MARKER,
207
+ summarizeRiddleProofPrComment,
208
+ buildRiddleProofPrCommentMarkdown
209
+ };
@@ -1,3 +1,13 @@
1
+ import {
2
+ createRiddleApiClient,
3
+ isTerminalRiddleJobStatus,
4
+ parseRiddleViewport
5
+ } from "./chunk-DI2XNGEZ.js";
6
+ import {
7
+ RIDDLE_PROOF_PR_COMMENT_MARKER,
8
+ buildRiddleProofPrCommentMarkdown,
9
+ summarizeRiddleProofPrComment
10
+ } from "./chunk-6KYXX4OE.js";
1
11
  import {
2
12
  RIDDLE_PROOF_PROFILE_EVIDENCE_VERSION,
3
13
  assessRiddleProofProfileEvidence,
@@ -13,11 +23,6 @@ import {
13
23
  resolveRiddleProofProfileTargetUrl,
14
24
  resolveRiddleProofProfileTimeoutSec
15
25
  } from "./chunk-Z2LCVROU.js";
16
- import {
17
- createRiddleApiClient,
18
- isTerminalRiddleJobStatus,
19
- parseRiddleViewport
20
- } from "./chunk-DI2XNGEZ.js";
21
26
  import {
22
27
  createDisabledRiddleProofAgentAdapter,
23
28
  readRiddleProofRunStatus,
@@ -56,11 +61,14 @@ var KNOWN_CLI_OPTIONS = /* @__PURE__ */ new Set([
56
61
  "codexSandbox",
57
62
  "codexTimeoutMs",
58
63
  "command",
64
+ "commentMode",
59
65
  "continueWithStage",
60
66
  "createdAt",
61
67
  "decision",
62
68
  "defaultReviewer",
63
69
  "defaultShipMode",
70
+ "bodyFile",
71
+ "dryRun",
64
72
  "exclude",
65
73
  "format",
66
74
  "framework",
@@ -88,20 +96,25 @@ var KNOWN_CLI_OPTIONS = /* @__PURE__ */ new Set([
88
96
  "pack",
89
97
  "packFile",
90
98
  "profile",
99
+ "proofDir",
100
+ "pr",
91
101
  "progressEveryMs",
92
102
  "quiet",
93
103
  "readinessPath",
94
104
  "readinessTimeout",
95
105
  "reasonsJson",
106
+ "repo",
96
107
  "requestJson",
97
108
  "requiredJson",
98
109
  "responseJson",
99
110
  "resultFormat",
111
+ "resultJson",
100
112
  "resultsDir",
101
113
  "riddleEngineModuleUrl",
102
114
  "riddleProofDir",
103
115
  "route",
104
116
  "runDir",
117
+ "runResponse",
105
118
  "runner",
106
119
  "scriptFile",
107
120
  "sourceKind",
@@ -112,8 +125,10 @@ var KNOWN_CLI_OPTIONS = /* @__PURE__ */ new Set([
112
125
  "submitRetries",
113
126
  "submitTimeoutMs",
114
127
  "summary",
128
+ "successCriteria",
115
129
  "sync",
116
130
  "timeout",
131
+ "title",
117
132
  "url",
118
133
  "unsubmittedJobRetries",
119
134
  "unsubmittedJobTimeoutMs",
@@ -138,6 +153,7 @@ function usage() {
138
153
  " riddle-proof-loop run-profile aggregate --profile <file|json|-> --url <base-url> [--base-url <base-url>] --input-dir <dir>|--inputs <path[,path...]> [--output <dir>|--output-dir <dir>] [--result-format json|compact-json|summary|none; default json]",
139
154
  " riddle-proof-loop run-profile recover --profile <file|json|-> --url <base-url> [--base-url <base-url>] --job <job-id> [--viewport-name <name[,name...]>] [--output <dir>|--output-dir <dir>] [--result-format json|compact-json|summary|none; default json]",
140
155
  " riddle-proof-loop regression-pack run [--pack oc-flow-regression|--pack-file <file>] [--local-core true|false; default true] [--hosted-riddle true|false; default false] [--format json|markdown|compact-json; default json] [--output <dir>|--output-dir <dir>]",
156
+ " riddle-proof-loop pr-comment --proof-dir <dir>|--run-response <file> [--result-json <file>] --pr <number|url> [--repo owner/name] [--dry-run] [--body-file <file>] [--comment-mode update|append]",
141
157
  " riddle-proof-loop profile-body-assertions --artifact <file|url|-> --candidates-json <file|json|-> [--required-json <file|json|->] [--format json|body-contains]",
142
158
  " riddle-proof-loop profile-http-status-preflight --profile <file|json|-> --url <base-url> [--format json|summary]",
143
159
  " riddle-proof-loop riddle-preview-deploy <build-dir> <label> [--framework spa|static] [--quiet]",
@@ -946,6 +962,81 @@ function riddlePreviewProgressLine(snapshot) {
946
962
  const messagePart = snapshot.message ? ` ${snapshot.message}` : "";
947
963
  return `[riddle-preview] ${snapshot.stage}${idPart}${statusPart}${attemptPart} elapsed=${formatPollDuration(snapshot.elapsed_ms)} label=${snapshot.label} framework=${snapshot.framework}${filePart}${totalPart}${archivePart}${previewPart}${recoveryPart}${messagePart}`;
948
964
  }
965
+ function readJsonFileIfExists(filePath) {
966
+ if (!filePath || !existsSync(filePath)) return void 0;
967
+ return readJsonValue(filePath, filePath);
968
+ }
969
+ function defaultProofDirJsonPath(proofDir, filename) {
970
+ return proofDir ? path.join(proofDir, filename) : void 0;
971
+ }
972
+ function prNumberFromValue(value) {
973
+ const text = value?.trim();
974
+ if (!text) return "";
975
+ const match = text.match(/\/pull\/(\d+)(?:\/|$)/) || text.match(/^#?(\d+)$/);
976
+ return match ? match[1] : text;
977
+ }
978
+ function ghJson(args, input) {
979
+ const result = spawnSync("gh", args, {
980
+ input: typeof input === "undefined" ? void 0 : `${JSON.stringify(input)}
981
+ `,
982
+ encoding: "utf-8",
983
+ timeout: 9e4
984
+ });
985
+ if (result.error) throw result.error;
986
+ if (result.status !== 0) {
987
+ const detail = (result.stderr || result.stdout || "").trim();
988
+ throw new Error(`gh ${args.join(" ")} failed${detail ? `: ${detail}` : ""}`);
989
+ }
990
+ const stdout = (result.stdout || "").trim();
991
+ return stdout ? JSON.parse(stdout) : {};
992
+ }
993
+ function resolveGhRepoName(repoOption) {
994
+ if (repoOption?.trim()) return repoOption.trim();
995
+ const result = spawnSync("gh", ["repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"], {
996
+ encoding: "utf-8",
997
+ timeout: 3e4
998
+ });
999
+ if (result.error) throw result.error;
1000
+ if (result.status !== 0 || !result.stdout.trim()) {
1001
+ const detail = (result.stderr || result.stdout || "").trim();
1002
+ throw new Error(`Could not resolve GitHub repository. Pass --repo owner/name.${detail ? ` gh said: ${detail}` : ""}`);
1003
+ }
1004
+ return result.stdout.trim();
1005
+ }
1006
+ function findManagedPrComment(repo, prNumber) {
1007
+ const comments = ghJson(["api", `repos/${repo}/issues/${prNumber}/comments`, "--paginate"]);
1008
+ return comments.find((comment) => typeof comment.body === "string" && comment.body.includes(RIDDLE_PROOF_PR_COMMENT_MARKER));
1009
+ }
1010
+ function upsertPrComment(input) {
1011
+ const payload = { body: input.body };
1012
+ if (input.mode !== "append") {
1013
+ const existing = findManagedPrComment(input.repo, input.prNumber);
1014
+ if (existing?.id) {
1015
+ return {
1016
+ action: "updated",
1017
+ comment: ghJson([
1018
+ "api",
1019
+ `repos/${input.repo}/issues/comments/${existing.id}`,
1020
+ "-X",
1021
+ "PATCH",
1022
+ "--input",
1023
+ "-"
1024
+ ], payload)
1025
+ };
1026
+ }
1027
+ }
1028
+ return {
1029
+ action: "created",
1030
+ comment: ghJson([
1031
+ "api",
1032
+ `repos/${input.repo}/issues/${input.prNumber}/comments`,
1033
+ "-X",
1034
+ "POST",
1035
+ "--input",
1036
+ "-"
1037
+ ], payload)
1038
+ };
1039
+ }
949
1040
  function readJsonValue(value, label) {
950
1041
  if (!value) throw new Error(`${label} is required.`);
951
1042
  const raw = value === "-" ? readStdin() : existsSync(value) ? readFileSync(value, "utf-8") : value;
@@ -4578,6 +4669,47 @@ async function main() {
4578
4669
  process.exitCode = result.ok ? 0 : 1;
4579
4670
  return;
4580
4671
  }
4672
+ if (command === "pr-comment") {
4673
+ const proofDir = optionString(options, "proofDir") || optionString(options, "outputDir") || positional[1];
4674
+ const runResponsePath = optionString(options, "runResponse") || defaultProofDirJsonPath(proofDir, "riddle-run-response.json");
4675
+ const resultJsonPath = optionString(options, "resultJson") || defaultProofDirJsonPath(proofDir, "result.json");
4676
+ const runResponse = readJsonFileIfExists(runResponsePath);
4677
+ const result = readJsonFileIfExists(resultJsonPath);
4678
+ if (!runResponse && !result) {
4679
+ throw new Error("pr-comment requires --proof-dir with riddle-run-response.json/result.json or explicit --run-response/--result-json.");
4680
+ }
4681
+ const body = buildRiddleProofPrCommentMarkdown({
4682
+ title: optionString(options, "title"),
4683
+ goal: optionString(options, "summary"),
4684
+ successCriteria: optionString(options, "successCriteria"),
4685
+ runResponse,
4686
+ result
4687
+ });
4688
+ const bodyFile = optionString(options, "bodyFile");
4689
+ if (bodyFile) writeFileSync(bodyFile, body);
4690
+ if (optionBoolean(options, "dryRun")) {
4691
+ process.stdout.write(body);
4692
+ return;
4693
+ }
4694
+ const prNumber = prNumberFromValue(optionString(options, "pr"));
4695
+ if (!prNumber) throw new Error("pr-comment requires --pr <number|url> unless --dry-run is used.");
4696
+ const commentMode = optionString(options, "commentMode") || "update";
4697
+ if (!["update", "append"].includes(commentMode)) throw new Error("--comment-mode must be update or append.");
4698
+ const repo = resolveGhRepoName(optionString(options, "repo"));
4699
+ const resultPayload = upsertPrComment({ repo, prNumber, body, mode: commentMode });
4700
+ const comment = resultPayload.comment;
4701
+ process.stdout.write(`${JSON.stringify({
4702
+ ok: true,
4703
+ action: resultPayload.action,
4704
+ repo,
4705
+ pr: prNumber,
4706
+ comment_url: cliString(comment.html_url) || null,
4707
+ summary: summarizeRiddleProofPrComment({ runResponse, result }),
4708
+ body_file: bodyFile || null
4709
+ }, null, 2)}
4710
+ `);
4711
+ return;
4712
+ }
4581
4713
  if (command === "profile-http-status-preflight") {
4582
4714
  const profile = normalizeProfileForCli(options);
4583
4715
  const result = await preflightRiddleProofProfileHttpStatusChecks(profile);
package/dist/cli/index.js CHANGED
@@ -1,6 +1,7 @@
1
- import "../chunk-F4HKK2YH.js";
2
- import "../chunk-Z2LCVROU.js";
1
+ import "../chunk-MVJBPCZ4.js";
3
2
  import "../chunk-DI2XNGEZ.js";
3
+ import "../chunk-6KYXX4OE.js";
4
+ import "../chunk-Z2LCVROU.js";
4
5
  import "../chunk-ZREWMTFA.js";
5
6
  import "../chunk-ZQWVXQKJ.js";
6
7
  import "../chunk-RDPG554T.js";