@riddledc/riddle-proof 0.8.64 → 0.8.65

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
@@ -179,6 +179,22 @@ or as a stronger proof base before a change loop.
179
179
  }
180
180
  ```
181
181
 
182
+ For early proof authoring, `profile-suggest` can turn a route plus changed file,
183
+ selector, and changed-text hints into a conservative draft profile:
184
+
185
+ ```sh
186
+ riddle-proof-loop profile-suggest \
187
+ --route /games/signal-sprint \
188
+ --changed-files src/Games/SignalSprint.jsx,src/Games/SignalSprint.css \
189
+ --selectors .signal-sprint-start \
190
+ --changed-text-json '[{"selector":".signal-sprint-start","text":"Proof Run"}]' \
191
+ --format profile
192
+ ```
193
+
194
+ The suggestion helper only drafts checks and artifacts; it does not produce a
195
+ proof verdict. Run the generated profile with a local or hosted runner before
196
+ treating the packet as evidence.
197
+
182
198
  Run a profile with the hosted Riddle runner:
183
199
 
184
200
  ```sh
@@ -0,0 +1,178 @@
1
+ import {
2
+ RIDDLE_PROOF_PROFILE_VERSION
3
+ } from "./chunk-EX7TO4I5.js";
4
+
5
+ // src/profile-suggestions.ts
6
+ var RIDDLE_PROOF_PROFILE_SUGGESTIONS_VERSION = "riddle-proof.profile-suggestions.v1";
7
+ var UI_FILE_PATTERN = /\.(css|html?|jsx?|tsx?|vue|svelte|mdx)$/i;
8
+ var VISUAL_ASSET_PATTERN = /\.(avif|gif|jpe?g|png|svg|webp)$/i;
9
+ function nonEmptyStrings(values) {
10
+ return Array.from(new Set(
11
+ (values || []).filter((value) => typeof value === "string").map((value) => value.trim()).filter(Boolean)
12
+ ));
13
+ }
14
+ function normalizeRoute(value) {
15
+ if (!value?.trim()) return void 0;
16
+ const trimmed = value.trim();
17
+ if (/^https?:\/\//i.test(trimmed)) return void 0;
18
+ return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
19
+ }
20
+ function routeFromUrl(value) {
21
+ if (!value?.trim()) return void 0;
22
+ try {
23
+ const url = new URL(value);
24
+ return normalizeRoute(`${url.pathname}${url.search || ""}`);
25
+ } catch {
26
+ return void 0;
27
+ }
28
+ }
29
+ function slugify(value) {
30
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "suggested-profile";
31
+ }
32
+ function inferRouteFromFile(file) {
33
+ const normalized = file.replace(/\\/g, "/");
34
+ const routeMatch = normalized.match(/(?:^|\/)(?:pages|routes|app)\/(.+?)(?:\.[^.\/]+)?$/);
35
+ if (!routeMatch) return void 0;
36
+ const route = routeMatch[1].replace(/\/index$/i, "").replace(/\/page$/i, "").replace(/\[[^/\]]+\]/g, ":param");
37
+ return normalizeRoute(route || "/");
38
+ }
39
+ function inferRouteFromFiles(files) {
40
+ for (const file of files) {
41
+ const route = inferRouteFromFile(file);
42
+ if (route) return route;
43
+ }
44
+ return void 0;
45
+ }
46
+ function uniqueChecks(checks) {
47
+ const seen = /* @__PURE__ */ new Set();
48
+ return checks.filter((check) => {
49
+ const key = JSON.stringify(check);
50
+ if (seen.has(key)) return false;
51
+ seen.add(key);
52
+ return true;
53
+ });
54
+ }
55
+ function changedTextEntryText(entry) {
56
+ return typeof entry === "string" ? entry.trim() : (entry.expected_text || entry.text || "").trim();
57
+ }
58
+ function changedTextEntrySelector(entry) {
59
+ return typeof entry === "string" ? void 0 : entry.selector?.trim() || void 0;
60
+ }
61
+ function changedTextEntryLabel(entry) {
62
+ return typeof entry === "string" ? void 0 : entry.label?.trim() || void 0;
63
+ }
64
+ function suggestionProfileName(input, route, files) {
65
+ if (input.name?.trim()) return input.name.trim();
66
+ return `suggested-${slugify(route || files[0] || "profile")}`;
67
+ }
68
+ function defaultViewports(includeMobile) {
69
+ const viewports = [
70
+ { name: "desktop", width: 1280, height: 800 }
71
+ ];
72
+ if (includeMobile !== false) {
73
+ viewports.push({ name: "mobile", width: 390, height: 844 });
74
+ }
75
+ return viewports;
76
+ }
77
+ function suggestRiddleProofProfileChecks(input) {
78
+ const changedFiles = nonEmptyStrings(input.changed_files);
79
+ const selectors = nonEmptyStrings(input.selectors);
80
+ const changedText = input.changed_text || [];
81
+ const route = normalizeRoute(input.route) || inferRouteFromFiles(changedFiles) || routeFromUrl(input.url);
82
+ const uiFiles = changedFiles.filter((file) => UI_FILE_PATTERN.test(file));
83
+ const visualAssets = changedFiles.filter((file) => VISUAL_ASSET_PATTERN.test(file));
84
+ const warnings = [];
85
+ const suggestions = [];
86
+ const setupActions = [];
87
+ if (!route && !input.url?.trim()) {
88
+ warnings.push("No route or URL was supplied; the draft profile defaults to route /.");
89
+ }
90
+ if ((uiFiles.length || visualAssets.length) && !changedText.length && !selectors.length) {
91
+ warnings.push("UI or visual files changed, but no changed text or selectors were supplied; add focused checks before trusting this profile.");
92
+ }
93
+ const routeOrDefault = route || "/";
94
+ const routeChecks = [
95
+ { type: "route_loaded", expected_path: routeOrDefault }
96
+ ];
97
+ suggestions.push({
98
+ id: "route-loaded",
99
+ reason: "Every browser proof should first prove that the intended route loaded.",
100
+ checks: routeChecks
101
+ });
102
+ const hygieneChecks = [
103
+ { type: "no_fatal_console_errors" },
104
+ { type: "no_mobile_horizontal_overflow" }
105
+ ];
106
+ suggestions.push({
107
+ id: "runtime-hygiene",
108
+ reason: "UI changes should not introduce fatal console errors or mobile overflow.",
109
+ checks: hygieneChecks
110
+ });
111
+ for (const selector of selectors) {
112
+ suggestions.push({
113
+ id: `selector-${slugify(selector)}`,
114
+ reason: `Changed selector ${selector} should remain visible.`,
115
+ checks: [{ type: "selector_visible", selector }]
116
+ });
117
+ }
118
+ for (const entry of changedText) {
119
+ const text = changedTextEntryText(entry);
120
+ if (!text) continue;
121
+ const selector = changedTextEntrySelector(entry);
122
+ const label = changedTextEntryLabel(entry);
123
+ const check = selector ? { type: "selector_text_visible", selector, text, label } : { type: "text_visible", text, label };
124
+ suggestions.push({
125
+ id: `text-${slugify(`${selector || "page"}-${text}`)}`,
126
+ reason: selector ? `Expected text should be visible inside ${selector}.` : "Expected text should be visible on the route.",
127
+ checks: [check]
128
+ });
129
+ }
130
+ if (input.include_screenshot !== false && (uiFiles.length || visualAssets.length || selectors.length || changedText.length)) {
131
+ setupActions.push({ type: "screenshot", label: "suggested-proof-screenshot", full_page: true });
132
+ suggestions.push({
133
+ id: "screenshot-artifact",
134
+ reason: "A screenshot artifact keeps the proof packet inspectable by humans and hosted renderers.",
135
+ checks: [],
136
+ setup_actions: setupActions
137
+ });
138
+ }
139
+ const checks = uniqueChecks(suggestions.flatMap((suggestion) => suggestion.checks));
140
+ const target = {
141
+ ...input.url?.trim() ? { url: input.url.trim() } : { route: routeOrDefault },
142
+ viewports: defaultViewports(input.include_mobile),
143
+ setup_actions: setupActions.length ? setupActions : void 0
144
+ };
145
+ const profile = {
146
+ version: RIDDLE_PROOF_PROFILE_VERSION,
147
+ name: suggestionProfileName(input, routeOrDefault, changedFiles),
148
+ target,
149
+ checks,
150
+ artifacts: ["screenshot", "console", "proof_json"],
151
+ baseline_policy: "invariant_only",
152
+ failure_policy: {
153
+ product_regression: "fail",
154
+ proof_insufficient: "review",
155
+ environment_blocked: "neutral",
156
+ configuration_error: "fail",
157
+ needs_human_review: "review"
158
+ },
159
+ metadata: {
160
+ suggestion_input: {
161
+ changed_files: changedFiles,
162
+ selectors,
163
+ changed_text_count: changedText.length
164
+ }
165
+ }
166
+ };
167
+ return {
168
+ version: RIDDLE_PROOF_PROFILE_SUGGESTIONS_VERSION,
169
+ profile,
170
+ suggestions,
171
+ warnings
172
+ };
173
+ }
174
+
175
+ export {
176
+ RIDDLE_PROOF_PROFILE_SUGGESTIONS_VERSION,
177
+ suggestRiddleProofProfileChecks
178
+ };
@@ -5,6 +5,7 @@ import { tmpdir } from "os";
5
5
  import path from "path";
6
6
  var DEFAULT_RIDDLE_API_BASE_URL = "https://api.riddledc.com";
7
7
  var DEFAULT_RIDDLE_API_KEY_FILE = "/tmp/riddle-api-key";
8
+ var RIDDLE_UNSUBMITTED_WAKE_HINT = "hosted worker may still be waking or waiting for a lease";
8
9
  var RiddleApiError = class extends Error {
9
10
  constructor(pathname, status, body) {
10
11
  super(`Riddle API ${pathname} failed HTTP ${status}: ${body}`);
@@ -472,9 +473,10 @@ function buildPollSnapshot(jobId, job, input) {
472
473
  function pollMessage(snapshot, timedOut) {
473
474
  if (!timedOut) return void 0;
474
475
  const submitted = snapshot.submitted_at || "not submitted";
476
+ const wakeHint = snapshot.running_without_submission && !snapshot.created_at && !snapshot.submitted_at ? ` ${RIDDLE_UNSUBMITTED_WAKE_HINT}.` : "";
475
477
  const queue = snapshot.queue_elapsed_ms !== null ? ` queue_elapsed_ms=${snapshot.queue_elapsed_ms}` : "";
476
478
  const preSubmit = snapshot.pre_submission_elapsed_ms > 0 ? ` pre_submission_elapsed_ms=${snapshot.pre_submission_elapsed_ms}` : "";
477
- return `Riddle job ${snapshot.job_id} did not reach a terminal status after ${snapshot.attempt} poll attempts; status=${snapshot.status || "unknown"} submitted_at=${submitted}.${queue}${preSubmit}`;
479
+ return `Riddle job ${snapshot.job_id} did not reach a terminal status after ${snapshot.attempt} poll attempts; status=${snapshot.status || "unknown"} submitted_at=${submitted}.${wakeHint}${queue}${preSubmit}`;
478
480
  }
479
481
  async function pollRiddleJob(config, jobId, options = {}) {
480
482
  if (!jobId?.trim()) throw new Error("jobId is required");
@@ -585,6 +587,7 @@ function createRiddleApiClient(config = {}) {
585
587
  export {
586
588
  DEFAULT_RIDDLE_API_BASE_URL,
587
589
  DEFAULT_RIDDLE_API_KEY_FILE,
590
+ RIDDLE_UNSUBMITTED_WAKE_HINT,
588
591
  RiddleApiError,
589
592
  resolveRiddleApiKeySource,
590
593
  resolveRiddleApiKey,
@@ -1,13 +1,17 @@
1
1
  import {
2
+ RIDDLE_UNSUBMITTED_WAKE_HINT,
2
3
  createRiddleApiClient,
3
4
  isTerminalRiddleJobStatus,
4
5
  parseRiddleViewport
5
- } from "./chunk-DI2XNGEZ.js";
6
+ } from "./chunk-B2DP2LET.js";
6
7
  import {
7
8
  RIDDLE_PROOF_PR_COMMENT_MARKER,
8
9
  buildRiddleProofPrCommentMarkdown,
9
10
  summarizeRiddleProofPrComment
10
11
  } from "./chunk-RZ3GXSXQ.js";
12
+ import {
13
+ suggestRiddleProofProfileChecks
14
+ } from "./chunk-5Y4V2IXI.js";
11
15
  import {
12
16
  RIDDLE_PROOF_PROFILE_EVIDENCE_VERSION,
13
17
  applyRiddleProofProfileArtifactCompleteness,
@@ -55,6 +59,8 @@ var KNOWN_CLI_OPTIONS = /* @__PURE__ */ new Set([
55
59
  "candidatesJson",
56
60
  "checkpointMode",
57
61
  "checkpointVisibility",
62
+ "changedFiles",
63
+ "changedTextJson",
58
64
  "codexCommand",
59
65
  "codexFullAuto",
60
66
  "codexHome",
@@ -76,6 +82,8 @@ var KNOWN_CLI_OPTIONS = /* @__PURE__ */ new Set([
76
82
  "help",
77
83
  "hostedRiddle",
78
84
  "image",
85
+ "includeMobile",
86
+ "includeScreenshot",
79
87
  "input",
80
88
  "inputDir",
81
89
  "inputFile",
@@ -86,6 +94,7 @@ var KNOWN_CLI_OPTIONS = /* @__PURE__ */ new Set([
86
94
  "jobId",
87
95
  "localCore",
88
96
  "maxIterations",
97
+ "name",
89
98
  "navigationTimeout",
90
99
  "output",
91
100
  "outputDir",
@@ -118,6 +127,7 @@ var KNOWN_CLI_OPTIONS = /* @__PURE__ */ new Set([
118
127
  "runResponse",
119
128
  "runner",
120
129
  "scriptFile",
130
+ "selectors",
121
131
  "sourceKind",
122
132
  "splitViewports",
123
133
  "stateDir",
@@ -153,6 +163,7 @@ function usage() {
153
163
  " riddle-proof-loop run-profile --profile <file|json|-> --url <base-url> [--base-url <base-url>] [--runner riddle] [--viewport-name <name[,name...]>] [--strict true|false; default false] [--split-viewports true|false; default false] [--balance-preflight true|false; default true] [--poll-attempts n] [--output <dir>|--output-dir <dir>] [--result-format json|compact-json|summary|none; default json] [--quiet]",
154
164
  " 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]",
155
165
  " 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]",
166
+ " riddle-proof-loop profile-suggest --route /path|--url <url> [--changed-files a,b] [--selectors .a,.b] [--changed-text-json <file|json|->] [--format json|profile]",
156
167
  " 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>]",
157
168
  " 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]",
158
169
  " riddle-proof-loop profile-body-assertions --artifact <file|url|-> --candidates-json <file|json|-> [--required-json <file|json|->] [--format json|body-contains]",
@@ -207,6 +218,10 @@ function optionString(options, key) {
207
218
  const value = options[key];
208
219
  return typeof value === "string" && value.trim() ? value.trim() : void 0;
209
220
  }
221
+ function optionStringList(options, key) {
222
+ const value = optionString(options, key);
223
+ return value ? value.split(",").map((item) => item.trim()).filter(Boolean) : void 0;
224
+ }
210
225
  function optionBoolean(options, key) {
211
226
  const value = options[key];
212
227
  if (typeof value === "undefined") return void 0;
@@ -940,7 +955,7 @@ function formatByteCount(bytes) {
940
955
  }
941
956
  function riddlePollProgressLine(snapshot) {
942
957
  const submittedAt = snapshot.submitted_at || "not-submitted";
943
- const queuePart = snapshot.running_without_submission ? ` waiting_for_submit=${formatPollDuration(snapshot.pre_submission_elapsed_ms)}${snapshot.queue_elapsed_ms !== null ? ` queued_for=${formatPollDuration(snapshot.queue_elapsed_ms)}` : ""}` : snapshot.queue_elapsed_ms !== null ? ` queue=${formatPollDuration(snapshot.queue_elapsed_ms)}` : "";
958
+ const queuePart = snapshot.running_without_submission ? ` waiting_for_worker_submit=${formatPollDuration(snapshot.pre_submission_elapsed_ms)} worker_wake=possible${snapshot.queue_elapsed_ms !== null ? ` queued_for=${formatPollDuration(snapshot.queue_elapsed_ms)}` : ""}` : snapshot.queue_elapsed_ms !== null ? ` queue=${formatPollDuration(snapshot.queue_elapsed_ms)}` : "";
944
959
  const terminalPart = snapshot.terminal ? " terminal=true" : "";
945
960
  return [
946
961
  "[riddle-poll]",
@@ -1047,6 +1062,11 @@ function readJsonValue(value, label) {
1047
1062
  }
1048
1063
  return parsed;
1049
1064
  }
1065
+ function readJsonAnyValue(value, label) {
1066
+ if (!value) throw new Error(`${label} is required.`);
1067
+ const raw = value === "-" ? readStdin() : existsSync(value) ? readFileSync(value, "utf-8") : value;
1068
+ return JSON.parse(raw);
1069
+ }
1050
1070
  function readOptionalJsonRecord(value, label) {
1051
1071
  if (!value) return void 0;
1052
1072
  return readJsonValue(value, label);
@@ -1361,7 +1381,7 @@ function profileRiddleJobMarkdown(result) {
1361
1381
  lines.push("- artifact recovery: used artifacts endpoint after non-terminal poll");
1362
1382
  }
1363
1383
  if (retryCount !== void 0 && retryCount > 0) {
1364
- lines.push(`- retry recovery: replaced ${retryCount} unsubmitted job${retryCount === 1 ? "" : "s"}${staleJobIds.length ? ` (${staleJobIds.map((value) => markdownInlineCode(value)).join(", ")})` : ""}`);
1384
+ lines.push(`- retry recovery: replaced ${retryCount} unsubmitted hosted job${retryCount === 1 ? "" : "s"}${staleJobIds.length ? ` (${staleJobIds.map((value) => markdownInlineCode(value)).join(", ")})` : ""}; ${RIDDLE_UNSUBMITTED_WAKE_HINT}`);
1365
1385
  }
1366
1386
  for (const job of splitJobs.slice(0, 12)) {
1367
1387
  const viewport = cliString(job.viewport) || "viewport";
@@ -4446,7 +4466,7 @@ async function runSingleRiddleProfileForCli(profile, options, input) {
4446
4466
  }
4447
4467
  staleJobIds.push(jobId);
4448
4468
  if (options.quiet !== true) {
4449
- process.stderr.write(`[riddle-poll] ${jobId} stayed unsubmitted for ${formatPollDuration(poll.poll?.pre_submission_elapsed_ms)}; retrying hosted run ${attempt + 1}/${retryLimit}
4469
+ process.stderr.write(`[riddle-poll] ${jobId} stayed unsubmitted for ${formatPollDuration(poll.poll?.pre_submission_elapsed_ms)}; ${RIDDLE_UNSUBMITTED_WAKE_HINT}; retrying hosted run ${attempt + 1}/${retryLimit}
4450
4470
  `);
4451
4471
  }
4452
4472
  continue;
@@ -4747,6 +4767,34 @@ async function main() {
4747
4767
  process.exitCode = result.ok ? 0 : 1;
4748
4768
  return;
4749
4769
  }
4770
+ if (command === "profile-suggest") {
4771
+ const changedTextJson = optionString(options, "changedTextJson");
4772
+ const changedText = changedTextJson ? readJsonAnyValue(changedTextJson, "--changed-text-json") : void 0;
4773
+ if (changedText !== void 0 && !Array.isArray(changedText)) {
4774
+ throw new Error("--changed-text-json must be a JSON array.");
4775
+ }
4776
+ const result = suggestRiddleProofProfileChecks({
4777
+ name: optionString(options, "name"),
4778
+ route: optionString(options, "route"),
4779
+ url: optionString(options, "url"),
4780
+ changed_files: optionStringList(options, "changedFiles"),
4781
+ selectors: optionStringList(options, "selectors"),
4782
+ changed_text: changedText,
4783
+ include_mobile: optionBoolean(options, "includeMobile"),
4784
+ include_screenshot: optionBoolean(options, "includeScreenshot")
4785
+ });
4786
+ const format = optionString(options, "format") || "json";
4787
+ if (format === "profile") {
4788
+ process.stdout.write(`${JSON.stringify(result.profile, null, 2)}
4789
+ `);
4790
+ } else if (format === "json") {
4791
+ process.stdout.write(`${JSON.stringify(result, null, 2)}
4792
+ `);
4793
+ } else {
4794
+ throw new Error("--format must be json or profile.");
4795
+ }
4796
+ return;
4797
+ }
4750
4798
  if (command === "profile-body-assertions") {
4751
4799
  const artifactText = await readTextValue(optionString(options, "artifact") ?? optionString(options, "input"), "--artifact");
4752
4800
  const candidates = readOptionalJsonStringArray(optionString(options, "candidatesJson") ?? optionString(options, "candidateJson"), "--candidates-json") ?? [];
package/dist/cli/index.js CHANGED
@@ -1,6 +1,7 @@
1
- import "../chunk-DDAV6D6K.js";
2
- import "../chunk-DI2XNGEZ.js";
1
+ import "../chunk-JPRKO2DH.js";
2
+ import "../chunk-B2DP2LET.js";
3
3
  import "../chunk-RZ3GXSXQ.js";
4
+ import "../chunk-5Y4V2IXI.js";
4
5
  import "../chunk-EX7TO4I5.js";
5
6
  import "../chunk-P4BHU5NM.js";
6
7
  import "../chunk-NMQIWBSB.js";