@riddledc/riddle-proof 0.8.33 → 0.8.34

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@riddledc/riddle-proof",
3
- "version": "0.8.33",
3
+ "version": "0.8.34",
4
4
  "description": "Reusable Riddle Proof contracts and helpers for evidence-backed agent changes.",
5
5
  "license": "MIT",
6
6
  "author": "RiddleDC",
@@ -320,7 +320,20 @@ def local_artifact_sources_fingerprint(local_sources):
320
320
  def public_proof_artifacts(state):
321
321
  published = state.get('proof_artifact_publication') or {}
322
322
  artifacts = published.get('artifacts') if isinstance(published, dict) else []
323
- public = [artifact for artifact in artifacts or [] if isinstance(artifact, dict) and (artifact.get('raw_url') or artifact.get('html_url'))]
323
+ attachments = state.get('proof_image_attachments') or {}
324
+ attachment_artifacts = attachments.get('artifacts') if isinstance(attachments, dict) else []
325
+ public = [
326
+ {
327
+ **artifact,
328
+ 'raw_url': artifact.get('attachment_url'),
329
+ 'html_url': artifact.get('attachment_url'),
330
+ 'embeddable': True,
331
+ 'attachment': True,
332
+ }
333
+ for artifact in attachment_artifacts or []
334
+ if isinstance(artifact, dict) and artifact.get('attachment_url')
335
+ ]
336
+ public.extend([artifact for artifact in artifacts or [] if isinstance(artifact, dict) and (artifact.get('raw_url') or artifact.get('html_url'))])
324
337
  for artifact in collect_proof_artifact_sources(state):
325
338
  url = str(artifact.get('url') or '').strip()
326
339
  if is_http_url(url):
@@ -328,6 +341,7 @@ def public_proof_artifacts(state):
328
341
  **artifact,
329
342
  'raw_url': url,
330
343
  'html_url': url,
344
+ 'embeddable': True,
331
345
  'published': False,
332
346
  })
333
347
  deduped = []
@@ -346,6 +360,8 @@ def first_public_artifact_url(state, role, kind=None):
346
360
  continue
347
361
  if kind and artifact.get('kind') != kind:
348
362
  continue
363
+ if artifact.get('kind') == 'image' and artifact.get('embeddable') is False:
364
+ continue
349
365
  return artifact.get('raw_url') or artifact.get('html_url') or artifact.get('url') or ''
350
366
  return ''
351
367
 
@@ -362,11 +378,92 @@ def resolve_github_repo_name(repo_dir):
362
378
  return ''
363
379
 
364
380
 
381
+ def resolve_github_repo_private(repo_dir):
382
+ result = sp.run(['gh', 'repo', 'view', '--json', 'isPrivate', '--jq', '.isPrivate'],
383
+ cwd=repo_dir, capture_output=True, text=True, timeout=30)
384
+ if result.returncode == 0:
385
+ text = result.stdout.strip().lower()
386
+ if text in ('true', 'false'):
387
+ return text == 'true'
388
+ return None
389
+
390
+
365
391
  def github_file_url(repo_name, ref, path_value, mode='blob'):
366
392
  safe_path = urllib.parse.quote(str(path_value or '').lstrip('/'), safe='/._-')
367
393
  return 'https://github.com/' + repo_name + '/' + mode + '/' + ref + '/' + safe_path
368
394
 
369
395
 
396
+ def parse_github_image_markdown(line):
397
+ match = re.search(r'!\[[^\]]*\]\((https://github\.com/user-attachments/assets/[^)\s]+)\)', str(line or '').strip())
398
+ return match.group(1) if match else ''
399
+
400
+
401
+ def upload_local_images_to_github_attachments(state, repo_dir):
402
+ if truthy(os.environ.get('RIDDLE_PROOF_DISABLE_GITHUB_IMAGE_ATTACHMENTS')):
403
+ state['proof_image_attachments'] = {'ok': False, 'skipped': True, 'reason': 'disabled'}
404
+ save_state(state)
405
+ return state['proof_image_attachments']
406
+
407
+ image_sources = [
408
+ artifact for artifact in local_proof_artifact_sources(state)
409
+ if artifact.get('kind') == 'image' and artifact.get('path')
410
+ ]
411
+ if not image_sources:
412
+ state['proof_image_attachments'] = {'ok': True, 'skipped': True, 'reason': 'no local image artifacts'}
413
+ save_state(state)
414
+ return state['proof_image_attachments']
415
+
416
+ repo_name = resolve_github_repo_name(repo_dir)
417
+ if not repo_name:
418
+ state['proof_image_attachments'] = {'ok': False, 'skipped': True, 'reason': 'repo name unavailable'}
419
+ save_state(state)
420
+ return state['proof_image_attachments']
421
+
422
+ timeout_seconds = int(os.environ.get('RIDDLE_PROOF_GH_IMAGE_TIMEOUT_SECONDS') or '15')
423
+ command = ['gh', 'image'] + [artifact.get('path') for artifact in image_sources] + ['--repo', repo_name]
424
+ try:
425
+ result = sp.run(command, cwd=repo_dir, capture_output=True, text=True, timeout=timeout_seconds)
426
+ except sp.TimeoutExpired:
427
+ state['proof_image_attachments'] = {'ok': False, 'skipped': True, 'reason': 'gh image timed out'}
428
+ save_state(state)
429
+ return state['proof_image_attachments']
430
+
431
+ if result.returncode != 0:
432
+ state['proof_image_attachments'] = {
433
+ 'ok': False,
434
+ 'skipped': True,
435
+ 'reason': 'gh image failed',
436
+ 'stderr': (result.stderr or result.stdout or '')[:500],
437
+ }
438
+ save_state(state)
439
+ return state['proof_image_attachments']
440
+
441
+ urls = [parse_github_image_markdown(line) for line in result.stdout.splitlines()]
442
+ urls = [url for url in urls if url]
443
+ if not urls:
444
+ state['proof_image_attachments'] = {'ok': False, 'skipped': True, 'reason': 'gh image returned no attachment URLs'}
445
+ save_state(state)
446
+ return state['proof_image_attachments']
447
+
448
+ artifacts = []
449
+ for index, artifact in enumerate(image_sources):
450
+ if index >= len(urls):
451
+ break
452
+ artifacts.append({
453
+ **artifact,
454
+ 'attachment_url': urls[index],
455
+ 'attachment': True,
456
+ 'embeddable': True,
457
+ })
458
+ state['proof_image_attachments'] = {
459
+ 'ok': True,
460
+ 'repo': repo_name,
461
+ 'artifacts': artifacts,
462
+ }
463
+ save_state(state)
464
+ return state['proof_image_attachments']
465
+
466
+
370
467
  def write_artifact_readme(path_value, state, artifacts):
371
468
  lines = [
372
469
  '# Riddle Proof Artifacts',
@@ -408,6 +505,7 @@ def publish_local_proof_artifacts_to_github(state, repo_dir, pr_num):
408
505
  repo_name = resolve_github_repo_name(repo_dir)
409
506
  if not repo_name:
410
507
  raise SystemExit('Could not resolve GitHub repository name for proof artifact publication.')
508
+ repo_private = resolve_github_repo_private(repo_dir)
411
509
 
412
510
  run_id = safe_slug(state.get('run_id') or str(int(time.time())), 'run')
413
511
  pr_slug = safe_slug('pr-' + str(pr_num or 'unknown'), 'pr')
@@ -474,11 +572,13 @@ def publish_local_proof_artifacts_to_github(state, repo_dir, pr_num):
474
572
  published_path = artifact.get('published_path')
475
573
  artifact['raw_url'] = github_file_url(repo_name, commit, published_path, 'raw')
476
574
  artifact['html_url'] = github_file_url(repo_name, commit, published_path, 'blob')
575
+ artifact['embeddable'] = False if repo_private is True and artifact.get('kind') == 'image' else True
477
576
  publication = {
478
577
  'ok': True,
479
578
  'branch': artifact_branch,
480
579
  'commit': commit,
481
580
  'repo': repo_name,
581
+ 'repo_private': repo_private,
482
582
  'html_url': github_file_url(repo_name, commit, artifact_dir_name, 'tree'),
483
583
  'manifest_url': github_file_url(repo_name, commit, artifact_dir_name + '/proof-artifacts.json', 'blob'),
484
584
  'readme_url': github_file_url(repo_name, commit, artifact_dir_name + '/README.md', 'blob'),
@@ -1080,6 +1180,12 @@ if publication.get('ok') and not publication.get('skipped'):
1080
1180
  elif publication.get('skipped'):
1081
1181
  print('Proof artifact publication skipped: ' + publication.get('reason', 'unknown'))
1082
1182
 
1183
+ image_attachments = upload_local_images_to_github_attachments(s, repo_dir)
1184
+ if image_attachments.get('ok') and not image_attachments.get('skipped'):
1185
+ print('Proof images attached: ' + str(len(image_attachments.get('artifacts') or [])))
1186
+ elif image_attachments.get('skipped'):
1187
+ print('Proof image attachment skipped: ' + image_attachments.get('reason', 'unknown'))
1188
+
1083
1189
  # Post proof comment on PR
1084
1190
  body = '## Riddle Proof — Proof of Fix\n\n'
1085
1191
  body += '**Goal:** ' + s.get('change_request', '') + '\n\n'
@@ -1099,15 +1205,34 @@ if publication.get('ok') and not publication.get('skipped'):
1099
1205
  before_image = first_public_artifact_url(s, 'before', 'image')
1100
1206
  prod_image = first_public_artifact_url(s, 'prod', 'image')
1101
1207
  after_image = first_public_artifact_url(s, 'after', 'image')
1208
+ linked_image_artifacts = [
1209
+ artifact for artifact in public_artifacts
1210
+ if artifact.get('kind') == 'image'
1211
+ and artifact.get('embeddable') is False
1212
+ and (artifact.get('html_url') or artifact.get('raw_url'))
1213
+ ]
1102
1214
  if before_image:
1103
1215
  body += '### Before\n![before](' + before_image + ')\n\n'
1104
1216
  if prod_image:
1105
1217
  body += '### Prod\n![prod](' + prod_image + ')\n\n'
1106
1218
  if after_image:
1107
1219
  body += '### After\n![after](' + after_image + ')\n\n'
1220
+ elif linked_image_artifacts:
1221
+ body += '### After evidence\nA screenshot artifact was captured, but GitHub attachment upload was unavailable; linked image artifacts are listed below.\n\n'
1108
1222
  else:
1109
1223
  body += '### After evidence\nNo after screenshot was captured for this verification mode; structured evidence is summarized below.\n\n'
1110
1224
 
1225
+ if linked_image_artifacts:
1226
+ body += '### Image artifacts\n'
1227
+ for artifact in linked_image_artifacts[:12]:
1228
+ label = artifact.get('name') or artifact.get('filename') or 'image'
1229
+ url = artifact.get('html_url') or artifact.get('raw_url')
1230
+ body += '- [' + str(label) + '](' + str(url) + ')'
1231
+ if artifact.get('role'):
1232
+ body += ' (' + str(artifact.get('role')) + ')'
1233
+ body += '\n'
1234
+ body += '\n'
1235
+
1111
1236
  data_artifacts = [
1112
1237
  artifact for artifact in public_artifacts
1113
1238
  if artifact.get('kind') != 'image' and (artifact.get('html_url') or artifact.get('raw_url'))
@@ -27,7 +27,15 @@ import sys
27
27
 
28
28
  args = sys.argv[1:]
29
29
  if args[:3] == ["repo", "view", "--json"]:
30
- print("example/test-repo")
30
+ if "isPrivate" in args:
31
+ print("true")
32
+ else:
33
+ print("example/test-repo")
34
+ raise SystemExit(0)
35
+ if args and args[0] == "image":
36
+ image_paths = [arg for arg in args[1:] if not arg.startswith("--") and arg != "example/test-repo"]
37
+ for index, image_path in enumerate(image_paths):
38
+ print(f"![{os.path.basename(image_path)}](https://github.com/user-attachments/assets/00000000-0000-4000-8000-{index:012d})")
31
39
  raise SystemExit(0)
32
40
  if args[:2] == ["pr", "list"]:
33
41
  print("")
@@ -163,16 +171,16 @@ def main():
163
171
  assert publication.get("ok") is True, "proof artifact publication should be recorded"
164
172
  assert publication.get("artifacts"), "published artifact list should be recorded"
165
173
  assert updated.get("ship_report", {}).get("after_artifact_url", "").startswith(
166
- "https://github.com/example/test-repo/raw/"
167
- ), "ship report should expose a GitHub-hosted after artifact URL"
174
+ "https://github.com/user-attachments/assets/"
175
+ ), "ship report should expose a GitHub-hosted attachment URL for the after artifact"
168
176
 
169
177
  comment = comment_body_path.read_text(encoding="utf-8")
170
178
  assert "file://" not in comment, "PR proof comment must not expose local file URLs"
171
179
  assert "raw.githubusercontent.com" not in comment, (
172
180
  "PR proof comment must not depend on unauthenticated raw GitHub URLs"
173
181
  )
174
- assert "![after](https://github.com/example/test-repo/raw/" in comment, (
175
- "PR proof comment should embed the GitHub-hosted after screenshot using a private-repo-safe URL"
182
+ assert "![after](https://github.com/user-attachments/assets/" in comment, (
183
+ "PR proof comment should embed the GitHub user-attachments screenshot when available"
176
184
  )
177
185
  assert "[proof.json](https://github.com/example/test-repo/blob/" in comment, (
178
186
  "PR proof comment should link the structured proof JSON"