hex-audit 0.4.4 → 0.4.5

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 (89) hide show
  1. package/DEMO.md +3 -2
  2. package/README.md +45 -41
  3. package/dashboard/.next/BUILD_ID +1 -1
  4. package/dashboard/.next/app-build-manifest.json +18 -11
  5. package/dashboard/.next/app-path-routes-manifest.json +1 -1
  6. package/dashboard/.next/build-manifest.json +4 -4
  7. package/dashboard/.next/next-server.js.nft.json +1 -1
  8. package/dashboard/.next/prerender-manifest.js +1 -1
  9. package/dashboard/.next/prerender-manifest.json +1 -1
  10. package/dashboard/.next/required-server-files.json +1 -1
  11. package/dashboard/.next/routes-manifest.json +1 -1
  12. package/dashboard/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  13. package/dashboard/.next/server/app/_not-found.html +1 -1
  14. package/dashboard/.next/server/app/_not-found.rsc +4 -4
  15. package/dashboard/.next/server/app/access/page_client-reference-manifest.js +1 -1
  16. package/dashboard/.next/server/app/api/flows/route.js +1 -1
  17. package/dashboard/.next/server/app/api/issues/[id]/move/route.js +1 -1
  18. package/dashboard/.next/server/app/api/issues/[id]/route.js +1 -1
  19. package/dashboard/.next/server/app/calls/page_client-reference-manifest.js +1 -1
  20. package/dashboard/.next/server/app/conformance/page_client-reference-manifest.js +1 -1
  21. package/dashboard/.next/server/app/diagram/page_client-reference-manifest.js +1 -1
  22. package/dashboard/.next/server/app/diagram.html +1 -1
  23. package/dashboard/.next/server/app/diagram.rsc +4 -4
  24. package/dashboard/.next/server/app/flows/page_client-reference-manifest.js +1 -1
  25. package/dashboard/.next/server/app/flows.html +1 -1
  26. package/dashboard/.next/server/app/flows.rsc +4 -4
  27. package/dashboard/.next/server/app/functions/page_client-reference-manifest.js +1 -1
  28. package/dashboard/.next/server/app/issues/page.js +12 -1
  29. package/dashboard/.next/server/app/issues/page_client-reference-manifest.js +1 -1
  30. package/dashboard/.next/server/app/overleaf/page.js +1 -0
  31. package/dashboard/.next/server/app/overleaf/page.js.nft.json +1 -0
  32. package/dashboard/.next/server/app/overleaf/page_client-reference-manifest.js +1 -0
  33. package/dashboard/.next/server/app/page_client-reference-manifest.js +1 -1
  34. package/dashboard/.next/server/app/progress/page_client-reference-manifest.js +1 -1
  35. package/dashboard/.next/server/app/stats/page_client-reference-manifest.js +1 -1
  36. package/dashboard/.next/server/app-paths-manifest.json +8 -7
  37. package/dashboard/.next/server/chunks/431.js +1 -1
  38. package/dashboard/.next/server/functions-config-manifest.json +1 -1
  39. package/dashboard/.next/server/middleware-build-manifest.js +1 -1
  40. package/dashboard/.next/server/pages/404.html +1 -1
  41. package/dashboard/.next/server/pages/500.html +1 -1
  42. package/dashboard/.next/server/server-reference-manifest.js +1 -1
  43. package/dashboard/.next/server/server-reference-manifest.json +1 -1
  44. package/dashboard/.next/static/chunks/app/issues/page-30f7bcde0f4f6ddb.js +1 -0
  45. package/dashboard/.next/static/chunks/app/{layout-527d8aa86e483fbd.js → layout-cde7d75ea6df4f81.js} +1 -1
  46. package/dashboard/.next/static/chunks/app/overleaf/page-bcea12a80c97b92f.js +1 -0
  47. package/dashboard/.next/static/chunks/{main-70c6d841ad572657.js → main-8f1e837090aef5cf.js} +1 -1
  48. package/dashboard/.next/static/css/36c6bb859d4545fd.css +3 -0
  49. package/dashboard/app/api/flows/route.ts +4 -0
  50. package/dashboard/app/api/issues/[id]/move/route.ts +29 -58
  51. package/dashboard/app/api/issues/[id]/route.ts +11 -102
  52. package/dashboard/app/issues/IssuesBoardClient.tsx +109 -35
  53. package/dashboard/app/issues/page.tsx +37 -3
  54. package/dashboard/app/overleaf/OverleafClient.tsx +97 -0
  55. package/dashboard/app/overleaf/page.tsx +32 -0
  56. package/dashboard/components/DashboardLayout.tsx +8 -2
  57. package/dashboard/lib/finding-markdown.ts +59 -0
  58. package/dashboard/next.config.mjs +4 -0
  59. package/dist/analysis/external-calls.d.ts +8 -1
  60. package/dist/analysis/external-calls.d.ts.map +1 -1
  61. package/dist/analysis/external-calls.js +57 -13
  62. package/dist/analysis/external-calls.js.map +1 -1
  63. package/dist/commands/calls.d.ts.map +1 -1
  64. package/dist/commands/calls.js +15 -6
  65. package/dist/commands/calls.js.map +1 -1
  66. package/dist/commands/init.d.ts.map +1 -1
  67. package/dist/commands/init.js +17 -19
  68. package/dist/commands/init.js.map +1 -1
  69. package/dist/commands/issue.d.ts +3 -0
  70. package/dist/commands/issue.d.ts.map +1 -0
  71. package/dist/commands/issue.js +115 -0
  72. package/dist/commands/issue.js.map +1 -0
  73. package/dist/core/issues.d.ts +79 -0
  74. package/dist/core/issues.d.ts.map +1 -0
  75. package/dist/core/issues.js +253 -0
  76. package/dist/core/issues.js.map +1 -0
  77. package/dist/index.js +3 -1
  78. package/dist/index.js.map +1 -1
  79. package/dist/skills/init-audit.md +3 -1
  80. package/dist/skills/sync-issues.md +17 -34
  81. package/dist/skills/validate-issue.md +20 -10
  82. package/dist/skills/write-finding.md +1 -1
  83. package/package.json +1 -1
  84. package/dashboard/.next/server/app/api/flows.body +0 -1
  85. package/dashboard/.next/server/app/api/flows.meta +0 -1
  86. package/dashboard/.next/static/chunks/app/issues/page-dc110d4cc0a5c398.js +0 -1
  87. package/dashboard/.next/static/css/7a38e5062ba6d403.css +0 -3
  88. /package/dashboard/.next/static/{ePObaFrrwft0BlvlllUMo → 4HWoeGmBSZi6iGcKL3VWA}/_buildManifest.js +0 -0
  89. /package/dashboard/.next/static/{ePObaFrrwft0BlvlllUMo → 4HWoeGmBSZi6iGcKL3VWA}/_ssgManifest.js +0 -0
@@ -0,0 +1,3 @@
1
+ *,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }/*
2
+ ! tailwindcss v3.4.19 | MIT License | https://tailwindcss.com
3
+ */*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:var(--font-sans);font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:var(--font-mono);font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.prose{color:var(--tw-prose-body);max-width:65ch}.prose :where(p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em}.prose :where([class~=lead]):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-lead);font-size:1.25em;line-height:1.6;margin-top:1.2em;margin-bottom:1.2em}.prose :where(a):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-links);text-decoration:underline;font-weight:500}.prose :where(strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-bold);font-weight:600}.prose :where(a strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(blockquote strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(thead th strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(ol):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:decimal;margin-top:1.25em;margin-bottom:1.25em;padding-inline-start:1.625em}.prose :where(ol[type=A]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-alpha}.prose :where(ol[type=a]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-alpha}.prose :where(ol[type=A s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-alpha}.prose :where(ol[type=a s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-alpha}.prose :where(ol[type=I]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-roman}.prose :where(ol[type=i]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-roman}.prose :where(ol[type=I s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-roman}.prose :where(ol[type=i s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-roman}.prose :where(ol[type="1"]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:decimal}.prose :where(ul):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:disc;margin-top:1.25em;margin-bottom:1.25em;padding-inline-start:1.625em}.prose :where(ol>li):not(:where([class~=not-prose],[class~=not-prose] *))::marker{font-weight:400;color:var(--tw-prose-counters)}.prose :where(ul>li):not(:where([class~=not-prose],[class~=not-prose] *))::marker{color:var(--tw-prose-bullets)}.prose :where(dt):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;margin-top:1.25em}.prose :where(hr):not(:where([class~=not-prose],[class~=not-prose] *)){border-color:var(--tw-prose-hr);border-top-width:1px;margin-top:3em;margin-bottom:3em}.prose :where(blockquote):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:500;font-style:italic;color:var(--tw-prose-quotes);border-inline-start-width:.25rem;border-inline-start-color:var(--tw-prose-quote-borders);quotes:"\201C""\201D""\2018""\2019";margin-top:1.6em;margin-bottom:1.6em;padding-inline-start:1em}.prose :where(blockquote p:first-of-type):not(:where([class~=not-prose],[class~=not-prose] *)):before{content:open-quote}.prose :where(blockquote p:last-of-type):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:close-quote}.prose :where(h1):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:800;font-size:2.25em;margin-top:0;margin-bottom:.8888889em;line-height:1.1111111}.prose :where(h1 strong):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:900;color:inherit}.prose :where(h2):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:700;font-size:1.5em;margin-top:2em;margin-bottom:1em;line-height:1.3333333}.prose :where(h2 strong):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:800;color:inherit}.prose :where(h3):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;font-size:1.25em;margin-top:1.6em;margin-bottom:.6em;line-height:1.6}.prose :where(h3 strong):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:700;color:inherit}.prose :where(h4):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;margin-top:1.5em;margin-bottom:.5em;line-height:1.5}.prose :where(h4 strong):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:700;color:inherit}.prose :where(img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(picture):not(:where([class~=not-prose],[class~=not-prose] *)){display:block;margin-top:2em;margin-bottom:2em}.prose :where(video):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(kbd):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:500;font-family:inherit;color:var(--tw-prose-kbd);box-shadow:0 0 0 1px var(--tw-prose-kbd-shadows),0 3px 0 var(--tw-prose-kbd-shadows);font-size:.875em;border-radius:.3125rem;padding-top:.1875em;padding-inline-end:.375em;padding-bottom:.1875em;padding-inline-start:.375em}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-code);font-weight:600;font-size:.875em}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)):before{content:"`"}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:"`"}.prose :where(a code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(h1 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(h2 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-size:.875em}.prose :where(h3 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-size:.9em}.prose :where(h4 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(blockquote code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(thead th code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(pre):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-pre-code);background-color:var(--tw-prose-pre-bg);overflow-x:auto;font-weight:400;font-size:.875em;line-height:1.7142857;margin-top:1.7142857em;margin-bottom:1.7142857em;border-radius:.375rem;padding-top:.8571429em;padding-inline-end:1.1428571em;padding-bottom:.8571429em;padding-inline-start:1.1428571em}.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)){background-color:transparent;border-width:0;border-radius:0;padding:0;font-weight:inherit;color:inherit;font-size:inherit;font-family:inherit;line-height:inherit}.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)):before{content:none}.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:none}.prose :where(table):not(:where([class~=not-prose],[class~=not-prose] *)){width:100%;table-layout:auto;margin-top:2em;margin-bottom:2em;font-size:.875em;line-height:1.7142857}.prose :where(thead):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:1px;border-bottom-color:var(--tw-prose-th-borders)}.prose :where(thead th):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;vertical-align:bottom;padding-inline-end:.5714286em;padding-bottom:.5714286em;padding-inline-start:.5714286em}.prose :where(tbody tr):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:1px;border-bottom-color:var(--tw-prose-td-borders)}.prose :where(tbody tr:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:0}.prose :where(tbody td):not(:where([class~=not-prose],[class~=not-prose] *)){vertical-align:baseline}.prose :where(tfoot):not(:where([class~=not-prose],[class~=not-prose] *)){border-top-width:1px;border-top-color:var(--tw-prose-th-borders)}.prose :where(tfoot td):not(:where([class~=not-prose],[class~=not-prose] *)){vertical-align:top}.prose :where(th,td):not(:where([class~=not-prose],[class~=not-prose] *)){text-align:start}.prose :where(figure>*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0;margin-bottom:0}.prose :where(figcaption):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-captions);font-size:.875em;line-height:1.4285714;margin-top:.8571429em}.prose{--tw-prose-body:#374151;--tw-prose-headings:#111827;--tw-prose-lead:#4b5563;--tw-prose-links:#111827;--tw-prose-bold:#111827;--tw-prose-counters:#6b7280;--tw-prose-bullets:#d1d5db;--tw-prose-hr:#e5e7eb;--tw-prose-quotes:#111827;--tw-prose-quote-borders:#e5e7eb;--tw-prose-captions:#6b7280;--tw-prose-kbd:#111827;--tw-prose-kbd-shadows:rgba(17,24,39,.1);--tw-prose-code:#111827;--tw-prose-pre-code:#e5e7eb;--tw-prose-pre-bg:#1f2937;--tw-prose-th-borders:#d1d5db;--tw-prose-td-borders:#e5e7eb;--tw-prose-invert-body:#d1d5db;--tw-prose-invert-headings:#fff;--tw-prose-invert-lead:#9ca3af;--tw-prose-invert-links:#fff;--tw-prose-invert-bold:#fff;--tw-prose-invert-counters:#9ca3af;--tw-prose-invert-bullets:#4b5563;--tw-prose-invert-hr:#374151;--tw-prose-invert-quotes:#f3f4f6;--tw-prose-invert-quote-borders:#374151;--tw-prose-invert-captions:#9ca3af;--tw-prose-invert-kbd:#fff;--tw-prose-invert-kbd-shadows:hsla(0,0%,100%,.1);--tw-prose-invert-code:#fff;--tw-prose-invert-pre-code:#d1d5db;--tw-prose-invert-pre-bg:rgba(0,0,0,.5);--tw-prose-invert-th-borders:#4b5563;--tw-prose-invert-td-borders:#374151;font-size:1rem;line-height:1.75}.prose :where(picture>img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0;margin-bottom:0}.prose :where(li):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.5em;margin-bottom:.5em}.prose :where(ol>li):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:.375em}.prose :where(ul>li):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:.375em}.prose :where(.prose>ul>li p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.75em;margin-bottom:.75em}.prose :where(.prose>ul>li>p:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em}.prose :where(.prose>ul>li>p:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em}.prose :where(.prose>ol>li>p:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em}.prose :where(.prose>ol>li>p:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em}.prose :where(ul ul,ul ol,ol ul,ol ol):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.75em;margin-bottom:.75em}.prose :where(dl):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em}.prose :where(dd):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.5em;padding-inline-start:1.625em}.prose :where(hr+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(h2+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(h3+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(h4+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(thead th:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:0}.prose :where(thead th:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-end:0}.prose :where(tbody td,tfoot td):not(:where([class~=not-prose],[class~=not-prose] *)){padding-top:.5714286em;padding-inline-end:.5714286em;padding-bottom:.5714286em;padding-inline-start:.5714286em}.prose :where(tbody td:first-child,tfoot td:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:0}.prose :where(tbody td:last-child,tfoot td:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-end:0}.prose :where(figure):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(.prose>:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(.prose>:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:0}.visible{visibility:visible}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.bottom-3{bottom:.75rem}.bottom-full{bottom:100%}.left-0{left:0}.left-1\/2{left:50%}.left-3{left:.75rem}.right-3{right:.75rem}.top-0{top:0}.top-3{top:.75rem}.z-10{z-index:10}.z-20{z-index:20}.z-40{z-index:40}.z-50{z-index:50}.z-\[60\]{z-index:60}.z-\[9000\]{z-index:9000}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-1\.5{margin-bottom:.375rem}.mb-2{margin-bottom:.5rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mb-sp-2{margin-bottom:var(--space-2)}.mb-sp-3{margin-bottom:var(--space-3)}.mb-sp-4{margin-bottom:var(--space-4)}.mb-sp-5{margin-bottom:var(--space-5)}.mb-sp-6{margin-bottom:var(--space-6)}.ml-1{margin-left:.25rem}.ml-1\.5{margin-left:.375rem}.ml-2{margin-left:.5rem}.ml-auto{margin-left:auto}.mr-4{margin-right:1rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.mt-sp-2{margin-top:var(--space-2)}.mt-sp-3{margin-top:var(--space-3)}.line-clamp-2{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-1\.5{height:.375rem}.h-10{height:2.5rem}.h-12{height:3rem}.h-2\.5{height:.625rem}.h-3{height:.75rem}.h-3\.5{height:.875rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-9{height:2.25rem}.h-\[600px\]{height:600px}.h-\[700px\]{height:700px}.h-full{height:100%}.h-screen{height:100vh}.max-h-72{max-height:18rem}.max-h-\[70vh\]{max-height:70vh}.max-h-\[80vh\]{max-height:80vh}.max-h-\[90vh\]{max-height:90vh}.min-h-9{min-height:2.25rem}.min-h-screen{min-height:100vh}.w-1\.5{width:.375rem}.w-10{width:2.5rem}.w-12{width:3rem}.w-2\.5{width:.625rem}.w-24{width:6rem}.w-3{width:.75rem}.w-3\.5{width:.875rem}.w-32{width:8rem}.w-4{width:1rem}.w-48{width:12rem}.w-5{width:1.25rem}.w-8{width:2rem}.w-\[220px\]{width:220px}.w-full{width:100%}.max-w-2xl{max-width:42rem}.max-w-3xl{max-width:48rem}.max-w-6xl{max-width:72rem}.max-w-\[200px\]{max-width:200px}.max-w-lg{max-width:32rem}.max-w-none{max-width:none}.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}.-translate-x-1\/2{--tw-translate-x:-50%}.-translate-x-1\/2,.-translate-x-full{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-x-full{--tw-translate-x:-100%}.translate-x-0{--tw-translate-x:0px}.rotate-180,.translate-x-0{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rotate-180{--tw-rotate:180deg}.rotate-90{--tw-rotate:90deg}.rotate-90,.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes spin{to{transform:rotate(1turn)}}.animate-spin{animation:spin 1s linear infinite}.cursor-grab{cursor:grab}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.items-baseline{align-items:baseline}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-0\.5{gap:.125rem}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-2\.5{gap:.625rem}.gap-3{gap:.75rem}.gap-sp-2{gap:var(--space-2)}.gap-sp-3{gap:var(--space-3)}.gap-sp-4{gap:var(--space-4)}.gap-x-3{-moz-column-gap:.75rem;column-gap:.75rem}.gap-y-1{row-gap:.25rem}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-top-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px * var(--tw-divide-y-reverse))}.divide-border-subtle>:not([hidden])~:not([hidden]){border-color:var(--border-subtle)}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis}.truncate,.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.break-words{overflow-wrap:break-word}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-sm{border-radius:var(--radius-sm)}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-dashed{border-style:dashed}.border-\[var\(--accent\)\],.border-accent{border-color:var(--accent)}.border-border-default{border-color:var(--border-default)}.border-border-emphasis{border-color:var(--border-emphasis)}.border-border-subtle{border-color:var(--border-subtle)}.bg-\[var\(--accent\)\]{background-color:var(--accent)}.bg-\[var\(--critical\)\]{background-color:var(--critical)}.bg-\[var\(--medium\)\]{background-color:var(--medium)}.bg-\[var\(--success\)\]{background-color:var(--success)}.bg-accent{background-color:var(--accent)}.bg-accent-subtle{background-color:var(--accent-subtle)}.bg-surface-0{background-color:var(--surface-0)}.bg-surface-1{background-color:var(--surface-1)}.bg-surface-2{background-color:var(--surface-2)}.bg-surface-3{background-color:var(--surface-3)}.bg-text-tertiary{background-color:var(--text-tertiary)}.bg-transparent{background-color:transparent}.p-0\.5{padding:.125rem}.p-1{padding:.25rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.p-sp-4{padding:var(--space-4)}.p-sp-6{padding:var(--space-6)}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-sp-3{padding-left:var(--space-3);padding-right:var(--space-3)}.px-sp-4{padding-left:var(--space-4);padding-right:var(--space-4)}.px-sp-5{padding-left:var(--space-5);padding-right:var(--space-5)}.px-sp-6{padding-left:var(--space-6);padding-right:var(--space-6)}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.py-sp-2{padding-top:var(--space-2);padding-bottom:var(--space-2)}.py-sp-3{padding-top:var(--space-3);padding-bottom:var(--space-3)}.py-sp-4{padding-top:var(--space-4);padding-bottom:var(--space-4)}.py-sp-6{padding-top:var(--space-6);padding-bottom:var(--space-6)}.py-sp-8{padding-top:var(--space-8);padding-bottom:var(--space-8)}.pb-4{padding-bottom:1rem}.pt-16{padding-top:4rem}.pt-6{padding-top:1.5rem}.pt-\[12vh\]{padding-top:12vh}.pt-sp-2{padding-top:var(--space-2)}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.align-top{vertical-align:top}.font-mono{font-family:var(--font-mono)}.font-sans{font-family:var(--font-sans)}.text-\[11px\]{font-size:11px}.text-body{font-size:14px;line-height:1.55;font-weight:400}.text-caption{font-size:12px;line-height:1.4;font-weight:400}.text-display{font-size:28px;line-height:1.2;font-weight:600}.text-heading{font-size:15px;line-height:1.4;font-weight:500}.text-title{font-size:20px;line-height:1.3;font-weight:600}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) var(--tw-numeric-spacing) var(--tw-numeric-fraction)}.leading-relaxed{line-height:1.625}.tracking-wider{letter-spacing:.05em}.text-\[var\(--critical\)\]{color:var(--critical)}.text-\[var\(--high\)\]{color:var(--high)}.text-\[var\(--info\)\]{color:var(--info)}.text-\[var\(--low\)\]{color:var(--low)}.text-\[var\(--medium\)\]{color:var(--medium)}.text-\[var\(--neutral\)\]{color:var(--neutral)}.text-\[var\(--success\)\]{color:var(--success)}.text-accent{color:var(--accent)}.text-severity-critical{color:var(--critical)}.text-surface-0{color:var(--surface-0)}.text-text-primary{color:var(--text-primary)}.text-text-secondary{color:var(--text-secondary)}.text-text-tertiary{color:var(--text-tertiary)}.placeholder-text-tertiary::-moz-placeholder{color:var(--text-tertiary)}.placeholder-text-tertiary::placeholder{color:var(--text-tertiary)}.opacity-20{opacity:.2}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-75{opacity:.75}.opacity-90{opacity:.9}.shadow-2xl{--tw-shadow:0 25px 50px -12px rgba(0,0,0,.25);--tw-shadow-colored:0 25px 50px -12px var(--tw-shadow-color)}.shadow-2xl,.shadow-lg{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow:var(--shadow-lg);--tw-shadow-colored:var(--shadow-lg)}.shadow-sm{--tw-shadow:var(--shadow-sm);--tw-shadow-colored:var(--shadow-sm);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.outline-none{outline:2px solid transparent;outline-offset:2px}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}:root{--critical:oklch(0.58 0.22 25);--high:oklch(0.68 0.16 55);--medium:oklch(0.72 0.14 85);--low:oklch(0.58 0.14 250);--info:oklch(0.55 0.01 265);--success:oklch(0.60 0.16 145);--neutral:oklch(0.50 0.005 265);--font-sans:"Manrope",system-ui,-apple-system,sans-serif;--font-mono:"JetBrains Mono",ui-monospace,"SF Mono",monospace;--space-1:4px;--space-2:8px;--space-3:12px;--space-4:16px;--space-5:24px;--space-6:32px;--space-8:48px;--radius-sm:6px;--radius-md:8px;--radius-lg:12px;--overlay-bg:oklch(0.08 0.005 265/0.75);--shadow-sm:0 1px 2px oklch(0 0 0/0.05);--shadow-md:0 2px 8px oklch(0 0 0/0.08),0 1px 2px oklch(0 0 0/0.04);--shadow-lg:0 8px 24px oklch(0 0 0/0.12),0 2px 8px oklch(0 0 0/0.06)}[data-theme=dark]{--surface-0:oklch(0.13 0.005 265);--surface-1:oklch(0.17 0.006 265);--surface-2:oklch(0.21 0.006 265);--surface-3:oklch(0.26 0.006 265);--border-subtle:oklch(0.28 0.005 265/0.5);--border-default:oklch(0.32 0.006 265/0.55);--border-emphasis:oklch(0.42 0.01 265/0.65);--text-primary:oklch(0.93 0.005 265);--text-secondary:oklch(0.65 0.01 265);--text-tertiary:oklch(0.48 0.008 265);--accent:oklch(0.58 0.19 265);--accent-subtle:oklch(0.58 0.19 265/0.10);--accent-secondary:oklch(0.68 0.14 265);--accent-warm:oklch(0.68 0.16 55)}[data-theme=light]{--surface-0:oklch(0.97 0.003 265);--surface-1:oklch(1 0 0);--surface-2:oklch(0.96 0.004 265);--surface-3:oklch(0.93 0.005 265);--border-subtle:oklch(0.85 0.005 265/0.5);--border-default:oklch(0.80 0.006 265/0.45);--border-emphasis:oklch(0.70 0.01 265/0.5);--text-primary:oklch(0.15 0.01 265);--text-secondary:oklch(0.42 0.01 265);--text-tertiary:oklch(0.60 0.008 265);--accent:oklch(0.48 0.22 265);--accent-subtle:oklch(0.48 0.22 265/0.08);--accent-secondary:oklch(0.55 0.18 265);--accent-warm:oklch(0.58 0.16 55)}body{font-family:var(--font-sans);color:var(--text-primary);background:var(--surface-0);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-size:14px;line-height:1.55}.font-mono,code,pre{font-family:var(--font-mono)}:focus-visible{outline:2px solid var(--accent);outline-offset:2px}.transition-colors,[role=button],a,button,input,select,textarea{transition:color .2s ease,background-color .2s ease,border-color .2s ease,opacity .2s ease,box-shadow .2s ease}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:var(--border-default);border-radius:4px}::-webkit-scrollbar-thumb:hover{background:var(--border-emphasis)}@keyframes fade-in{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}.stagger>*{opacity:0;animation:fade-in .3s ease-out forwards}.stagger>:first-child{animation-delay:.03s}.stagger>:nth-child(2){animation-delay:.06s}.stagger>:nth-child(3){animation-delay:.09s}.stagger>:nth-child(4){animation-delay:.12s}.stagger>:nth-child(5){animation-delay:.15s}.stagger>:nth-child(6){animation-delay:.18s}.stagger>:nth-child(7){animation-delay:.21s}.stagger>:nth-child(8){animation-delay:.24s}@keyframes scale-in{0%{opacity:0;transform:scale(.92)}to{opacity:1;transform:scale(1)}}.scale-in{animation:scale-in .35s ease-out forwards}.mermaid svg{background:transparent!important}.first\:mt-0:first-child{margin-top:0}.hover\:border-\[var\(--accent\)\]:hover{border-color:var(--accent)}.hover\:border-border-default:hover{border-color:var(--border-default)}.hover\:bg-accent-secondary:hover{background-color:var(--accent-secondary)}.hover\:bg-surface-2:hover{background-color:var(--surface-2)}.hover\:bg-surface-3:hover{background-color:var(--surface-3)}.hover\:text-text-primary:hover{color:var(--text-primary)}.hover\:text-text-secondary:hover{color:var(--text-secondary)}.hover\:underline:hover{text-decoration-line:underline}.focus\:border-accent:focus{border-color:var(--accent)}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus-visible\:outline:focus-visible{outline-style:solid}.focus-visible\:outline-2:focus-visible{outline-width:2px}.focus-visible\:-outline-offset-2:focus-visible{outline-offset:-2px}.focus-visible\:outline-offset-2:focus-visible{outline-offset:2px}.focus-visible\:outline-accent:focus-visible{outline-color:var(--accent)}.active\:cursor-grabbing:active{cursor:grabbing}.disabled\:opacity-50:disabled{opacity:.5}.group:hover .group-hover\:text-accent{color:var(--accent)}.prose-headings\:text-text-primary :is(:where(h1,h2,h3,h4,h5,h6,th):not(:where([class~=not-prose],[class~=not-prose] *))){color:var(--text-primary)}.prose-p\:text-text-secondary :is(:where(p):not(:where([class~=not-prose],[class~=not-prose] *))){color:var(--text-secondary)}.prose-a\:text-accent :is(:where(a):not(:where([class~=not-prose],[class~=not-prose] *))){color:var(--accent)}.prose-a\:no-underline :is(:where(a):not(:where([class~=not-prose],[class~=not-prose] *))){text-decoration-line:none}.hover\:prose-a\:underline :is(:where(a):not(:where([class~=not-prose],[class~=not-prose] *))):hover{text-decoration-line:underline}.prose-blockquote\:border-border-emphasis :is(:where(blockquote):not(:where([class~=not-prose],[class~=not-prose] *))){border-color:var(--border-emphasis)}.prose-blockquote\:text-text-secondary :is(:where(blockquote):not(:where([class~=not-prose],[class~=not-prose] *))){color:var(--text-secondary)}.prose-strong\:text-text-primary :is(:where(strong):not(:where([class~=not-prose],[class~=not-prose] *))){color:var(--text-primary)}.prose-code\:text-accent :is(:where(code):not(:where([class~=not-prose],[class~=not-prose] *))){color:var(--accent)}.prose-code\:before\:content-none :is(:where(code):not(:where([class~=not-prose],[class~=not-prose] *))):before{--tw-content:none;content:var(--tw-content)}.prose-code\:after\:content-none :is(:where(code):not(:where([class~=not-prose],[class~=not-prose] *))):after{--tw-content:none;content:var(--tw-content)}.prose-pre\:border :is(:where(pre):not(:where([class~=not-prose],[class~=not-prose] *))){border-width:1px}.prose-pre\:border-border-default :is(:where(pre):not(:where([class~=not-prose],[class~=not-prose] *))){border-color:var(--border-default)}.prose-pre\:bg-surface-0 :is(:where(pre):not(:where([class~=not-prose],[class~=not-prose] *))){background-color:var(--surface-0)}.prose-li\:text-text-secondary :is(:where(li):not(:where([class~=not-prose],[class~=not-prose] *))){color:var(--text-secondary)}.prose-th\:text-text-primary :is(:where(th):not(:where([class~=not-prose],[class~=not-prose] *))){color:var(--text-primary)}.prose-td\:text-text-secondary :is(:where(td):not(:where([class~=not-prose],[class~=not-prose] *))){color:var(--text-secondary)}.prose-hr\:border-border-default :is(:where(hr):not(:where([class~=not-prose],[class~=not-prose] *))){border-color:var(--border-default)}@media (min-width:640px){.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}@media (min-width:768px){.md\:ml-\[220px\]{margin-left:220px}.md\:hidden{display:none}.md\:translate-x-0{--tw-translate-x:0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:pt-6{padding-top:1.5rem}}@media (min-width:1024px){.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.lg\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}}@media (min-width:1280px){.xl\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}@font-face{font-family:__Manrope_73ee6c;font-style:normal;font-weight:200 800;font-display:swap;src:url(/_next/static/media/438aa629764e75f3-s.woff2) format("woff2");unicode-range:u+0460-052f,u+1c80-1c8a,u+20b4,u+2de0-2dff,u+a640-a69f,u+fe2e-fe2f}@font-face{font-family:__Manrope_73ee6c;font-style:normal;font-weight:200 800;font-display:swap;src:url(/_next/static/media/875ae681bfde4580-s.woff2) format("woff2");unicode-range:u+0301,u+0400-045f,u+0490-0491,u+04b0-04b1,u+2116}@font-face{font-family:__Manrope_73ee6c;font-style:normal;font-weight:200 800;font-display:swap;src:url(/_next/static/media/51251f8b9793cdb3-s.woff2) format("woff2");unicode-range:u+0370-0377,u+037a-037f,u+0384-038a,u+038c,u+038e-03a1,u+03a3-03ff}@font-face{font-family:__Manrope_73ee6c;font-style:normal;font-weight:200 800;font-display:swap;src:url(/_next/static/media/e857b654a2caa584-s.woff2) format("woff2");unicode-range:u+0102-0103,u+0110-0111,u+0128-0129,u+0168-0169,u+01a0-01a1,u+01af-01b0,u+0300-0301,u+0303-0304,u+0308-0309,u+0323,u+0329,u+1ea0-1ef9,u+20ab}@font-face{font-family:__Manrope_73ee6c;font-style:normal;font-weight:200 800;font-display:swap;src:url(/_next/static/media/cc978ac5ee68c2b6-s.woff2) format("woff2");unicode-range:u+0100-02ba,u+02bd-02c5,u+02c7-02cc,u+02ce-02d7,u+02dd-02ff,u+0304,u+0308,u+0329,u+1d00-1dbf,u+1e00-1e9f,u+1ef2-1eff,u+2020,u+20a0-20ab,u+20ad-20c0,u+2113,u+2c60-2c7f,u+a720-a7ff}@font-face{font-family:__Manrope_73ee6c;font-style:normal;font-weight:200 800;font-display:swap;src:url(/_next/static/media/4c9affa5bc8f420e-s.p.woff2) format("woff2");unicode-range:u+00??,u+0131,u+0152-0153,u+02bb-02bc,u+02c6,u+02da,u+02dc,u+0304,u+0308,u+0329,u+2000-206f,u+20ac,u+2122,u+2191,u+2193,u+2212,u+2215,u+feff,u+fffd}@font-face{font-family:__Manrope_Fallback_73ee6c;src:local("Arial");ascent-override:103.31%;descent-override:29.07%;line-gap-override:0.00%;size-adjust:103.19%}.__className_73ee6c{font-family:__Manrope_73ee6c,__Manrope_Fallback_73ee6c;font-style:normal}.__variable_73ee6c{--font-sans:"__Manrope_73ee6c","__Manrope_Fallback_73ee6c"}@font-face{font-family:__JetBrains_Mono_3c557b;font-style:normal;font-weight:100 800;font-display:swap;src:url(/_next/static/media/886030b0b59bc5a7-s.woff2) format("woff2");unicode-range:u+0460-052f,u+1c80-1c8a,u+20b4,u+2de0-2dff,u+a640-a69f,u+fe2e-fe2f}@font-face{font-family:__JetBrains_Mono_3c557b;font-style:normal;font-weight:100 800;font-display:swap;src:url(/_next/static/media/0aa834ed78bf6d07-s.woff2) format("woff2");unicode-range:u+0301,u+0400-045f,u+0490-0491,u+04b0-04b1,u+2116}@font-face{font-family:__JetBrains_Mono_3c557b;font-style:normal;font-weight:100 800;font-display:swap;src:url(/_next/static/media/67957d42bae0796d-s.woff2) format("woff2");unicode-range:u+0370-0377,u+037a-037f,u+0384-038a,u+038c,u+038e-03a1,u+03a3-03ff}@font-face{font-family:__JetBrains_Mono_3c557b;font-style:normal;font-weight:100 800;font-display:swap;src:url(/_next/static/media/f911b923c6adde36-s.woff2) format("woff2");unicode-range:u+0102-0103,u+0110-0111,u+0128-0129,u+0168-0169,u+01a0-01a1,u+01af-01b0,u+0300-0301,u+0303-0304,u+0308-0309,u+0323,u+0329,u+1ea0-1ef9,u+20ab}@font-face{font-family:__JetBrains_Mono_3c557b;font-style:normal;font-weight:100 800;font-display:swap;src:url(/_next/static/media/939c4f875ee75fbb-s.woff2) format("woff2");unicode-range:u+0100-02ba,u+02bd-02c5,u+02c7-02cc,u+02ce-02d7,u+02dd-02ff,u+0304,u+0308,u+0329,u+1d00-1dbf,u+1e00-1e9f,u+1ef2-1eff,u+2020,u+20a0-20ab,u+20ad-20c0,u+2113,u+2c60-2c7f,u+a720-a7ff}@font-face{font-family:__JetBrains_Mono_3c557b;font-style:normal;font-weight:100 800;font-display:swap;src:url(/_next/static/media/bb3ef058b751a6ad-s.p.woff2) format("woff2");unicode-range:u+00??,u+0131,u+0152-0153,u+02bb-02bc,u+02c6,u+02da,u+02dc,u+0304,u+0308,u+0329,u+2000-206f,u+20ac,u+2122,u+2191,u+2193,u+2212,u+2215,u+feff,u+fffd}@font-face{font-family:__JetBrains_Mono_Fallback_3c557b;src:local("Arial");ascent-override:75.79%;descent-override:22.29%;line-gap-override:0.00%;size-adjust:134.59%}.__className_3c557b{font-family:__JetBrains_Mono_3c557b,__JetBrains_Mono_Fallback_3c557b;font-style:normal}.__variable_3c557b{--font-mono:"__JetBrains_Mono_3c557b","__JetBrains_Mono_Fallback_3c557b"}
@@ -3,6 +3,10 @@ import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { getOutputDirPath } from '@/lib/data';
5
5
 
6
+ // Read .hex/diagrams/ at request time. Without this, next build prerenders the
7
+ // route against an empty build-time .hex/ and ships a static `{"flows":[]}`.
8
+ export const dynamic = 'force-dynamic';
9
+
6
10
  export async function GET() {
7
11
  const outputDir = getOutputDirPath();
8
12
 
@@ -1,74 +1,45 @@
1
1
  import { NextRequest, NextResponse } from 'next/server';
2
- import { readJsonFile, writeJsonFile } from '@/lib/data';
2
+ import { getOutputDirPath } from '@/lib/data';
3
+ import { moveIssue, type BoardColumn, COLUMN_TO_STATUS } from '../../../../../../src/core/issues';
3
4
 
4
- interface TrackingEntry {
5
- id: string;
6
- status?: string;
7
- duplicate_of?: string | null;
8
- [key: string]: unknown;
9
- }
10
-
11
- interface TrackingFile {
12
- findings: TrackingEntry[];
13
- }
5
+ export const dynamic = 'force-dynamic';
14
6
 
15
- // Map a board column to the canonical tracking status it persists as.
16
- const COLUMN_TO_STATUS: Record<string, string> = {
17
- potential: 'pending_validation',
7
+ const STATUS_TO_COLUMN: Record<string, BoardColumn> = {
8
+ pending_validation: 'potential',
9
+ unverified: 'potential',
18
10
  verified: 'verified',
19
- invalid: 'rejected',
11
+ rejected: 'invalid',
20
12
  duplicate: 'duplicate',
21
13
  };
22
14
 
23
- const STATUS_VALUES = new Set([
24
- 'pending_validation',
25
- 'unverified',
26
- 'verified',
27
- 'rejected',
28
- 'duplicate',
29
- ]);
30
-
31
15
  export async function POST(request: NextRequest, { params }: { params: { id: string } }) {
32
- const id = params.id;
33
- const body = (await request.json()) as {
34
- column?: string;
35
- status?: string;
36
- duplicate_of?: string | null;
37
- };
16
+ const body = (await request.json()) as { column?: string; status?: string };
17
+
18
+ // Accept either a board column directly, or a tracking status that maps to one.
19
+ let column: BoardColumn | undefined;
20
+ if (body.column && body.column in COLUMN_TO_STATUS) {
21
+ column = body.column as BoardColumn;
22
+ } else if (body.status && STATUS_TO_COLUMN[body.status]) {
23
+ column = STATUS_TO_COLUMN[body.status];
24
+ }
38
25
 
39
- let nextStatus: string | undefined;
40
- if (body.status) {
41
- if (!STATUS_VALUES.has(body.status)) {
42
- return NextResponse.json({ error: `Invalid status: ${body.status}` }, { status: 400 });
43
- }
44
- nextStatus = body.status;
45
- } else if (body.column) {
46
- nextStatus = COLUMN_TO_STATUS[body.column];
47
- if (!nextStatus) {
48
- return NextResponse.json({ error: `Invalid column: ${body.column}` }, { status: 400 });
49
- }
50
- } else {
26
+ if (!column) {
51
27
  return NextResponse.json(
52
- { error: "Body must include either 'column' or 'status'" },
28
+ {
29
+ error:
30
+ "Body must include a valid 'column' (potential|verified|invalid|duplicate) or 'status'",
31
+ },
53
32
  { status: 400 },
54
33
  );
55
34
  }
56
35
 
57
- const tracking = readJsonFile<TrackingFile>('tracking.json') ?? { findings: [] };
58
- const entry = tracking.findings.find((e) => e.id === id);
59
- if (!entry) {
60
- return NextResponse.json({ error: `Issue ${id} not found in tracking.json` }, { status: 404 });
61
- }
62
-
63
- entry.status = nextStatus;
64
- if (body.duplicate_of !== undefined) {
65
- entry.duplicate_of = body.duplicate_of;
66
- }
67
- // Clearing duplicate_of when leaving the duplicate column reduces stale refs.
68
- if (nextStatus !== 'duplicate' && entry.duplicate_of) {
69
- entry.duplicate_of = null;
36
+ try {
37
+ const entry = moveIssue(getOutputDirPath(), params.id, column);
38
+ return NextResponse.json({ ok: true, id: params.id, status: entry.status });
39
+ } catch (err) {
40
+ return NextResponse.json(
41
+ { error: err instanceof Error ? err.message : String(err) },
42
+ { status: 400 },
43
+ );
70
44
  }
71
-
72
- writeJsonFile('tracking.json', tracking);
73
- return NextResponse.json({ ok: true, id, status: nextStatus });
74
45
  }
@@ -1,109 +1,18 @@
1
1
  import { NextRequest, NextResponse } from 'next/server';
2
- import { readJsonFile, writeJsonFile } from '@/lib/data';
2
+ import { getOutputDirPath } from '@/lib/data';
3
+ import { patchIssue } from '../../../../../src/core/issues';
3
4
 
4
- interface FindingsFile {
5
- findings: Array<{
6
- id: string;
7
- title?: string;
8
- severity?: string;
9
- description?: string;
10
- recommendation?: string;
11
- resolution?: string;
12
- update_from_client?: string;
13
- [key: string]: unknown;
14
- }>;
15
- }
16
-
17
- interface TrackingFile {
18
- findings: Array<{
19
- id: string;
20
- title?: string;
21
- severity?: string;
22
- notes?: string;
23
- [key: string]: unknown;
24
- }>;
25
- }
26
-
27
- const EDITABLE_FIELDS = new Set([
28
- 'title',
29
- 'severity',
30
- 'description',
31
- 'recommendation',
32
- 'resolution',
33
- 'update_from_client',
34
- 'notes',
35
- ]);
36
-
37
- const SEVERITY_VALUES = new Set(['Critical', 'High', 'Medium', 'Low', 'Info']);
38
- const RESOLUTION_VALUES = new Set([
39
- 'Fixed',
40
- 'Mitigated',
41
- 'Acknowledged',
42
- 'Not Fixed',
43
- 'Unresolved',
44
- ]);
5
+ export const dynamic = 'force-dynamic';
45
6
 
46
7
  export async function PATCH(request: NextRequest, { params }: { params: { id: string } }) {
47
- const id = params.id;
48
8
  const body = (await request.json()) as Record<string, unknown>;
49
-
50
- const updates: Record<string, unknown> = {};
51
- for (const [key, value] of Object.entries(body)) {
52
- if (!EDITABLE_FIELDS.has(key)) continue;
53
- if (key === 'severity' && typeof value === 'string' && !SEVERITY_VALUES.has(value)) {
54
- return NextResponse.json({ error: `Invalid severity: ${value}` }, { status: 400 });
55
- }
56
- if (
57
- key === 'resolution' &&
58
- value != null &&
59
- typeof value === 'string' &&
60
- !RESOLUTION_VALUES.has(value)
61
- ) {
62
- return NextResponse.json({ error: `Invalid resolution: ${value}` }, { status: 400 });
63
- }
64
- updates[key] = value;
65
- }
66
-
67
- if (Object.keys(updates).length === 0) {
68
- return NextResponse.json({ error: 'No editable fields supplied' }, { status: 400 });
9
+ try {
10
+ const finding = patchIssue(getOutputDirPath(), params.id, body);
11
+ return NextResponse.json({ ok: true, id: params.id, finding });
12
+ } catch (err) {
13
+ return NextResponse.json(
14
+ { error: err instanceof Error ? err.message : String(err) },
15
+ { status: 400 },
16
+ );
69
17
  }
70
-
71
- // Apply to findings.json if a matching record exists; otherwise we record the
72
- // edits on the tracking entry (title, severity, notes flow through; description
73
- // and recommendation can't be displayed yet but we still persist them so a
74
- // later /validate-issue or /write-finding can pick them up).
75
- const findings = readJsonFile<FindingsFile>('findings.json') ?? { findings: [] };
76
- const tracking = readJsonFile<TrackingFile>('tracking.json') ?? { findings: [] };
77
-
78
- let findingTouched = false;
79
- for (const finding of findings.findings) {
80
- if (finding.id !== id) continue;
81
- for (const [key, value] of Object.entries(updates)) {
82
- if (key === 'notes') continue; // notes lives on tracking, not findings
83
- finding[key] = value as never;
84
- }
85
- findingTouched = true;
86
- break;
87
- }
88
-
89
- let trackingTouched = false;
90
- for (const entry of tracking.findings) {
91
- if (entry.id !== id) continue;
92
- // Title and severity are mirrored on tracking so the board shows current
93
- // values without re-reading findings.json.
94
- if ('title' in updates) entry.title = updates.title as string;
95
- if ('severity' in updates) entry.severity = updates.severity as string;
96
- if ('notes' in updates) entry.notes = updates.notes as string;
97
- trackingTouched = true;
98
- break;
99
- }
100
-
101
- if (!findingTouched && !trackingTouched) {
102
- return NextResponse.json({ error: `Issue ${id} not found` }, { status: 404 });
103
- }
104
-
105
- if (findingTouched) writeJsonFile('findings.json', findings);
106
- if (trackingTouched) writeJsonFile('tracking.json', tracking);
107
-
108
- return NextResponse.json({ ok: true, id, updates });
109
18
  }
@@ -11,8 +11,10 @@ import {
11
11
  useDroppable,
12
12
  } from '@dnd-kit/core';
13
13
  import { SeverityBadge } from '@/components/SeverityBadge';
14
+ import { findingToHackmd, type FormatContext } from '@/lib/finding-markdown';
14
15
 
15
16
  export type BoardColumn = 'potential' | 'verified' | 'invalid' | 'duplicate';
17
+ export type SyncState = 'none' | 'unsynced' | 'synced_open' | 'synced_closed' | 'conflict';
16
18
  export type IssueStatus =
17
19
  | 'pending_validation'
18
20
  | 'unverified'
@@ -36,7 +38,10 @@ export interface BoardIssue {
36
38
  code_locations: Array<{ file: string; snippet?: string }>;
37
39
  github_synced: boolean;
38
40
  github_issue_url?: string;
41
+ github_issue_number?: number;
39
42
  github_state?: string;
43
+ /** Derived in page.tsx: how this verified finding stands vs GitHub. */
44
+ sync_state?: SyncState;
40
45
  duplicate_of: string | null;
41
46
  match_signals?: {
42
47
  contract: boolean;
@@ -70,7 +75,13 @@ const SEVERITY_ORDER: Record<BoardIssue['severity'], number> = {
70
75
  const SEVERITIES: BoardIssue['severity'][] = ['Critical', 'High', 'Medium', 'Low', 'Info'];
71
76
  const RESOLUTIONS = ['Fixed', 'Mitigated', 'Acknowledged', 'Not Fixed', 'Unresolved'];
72
77
 
73
- export function IssuesBoardClient({ issues: initialIssues }: { issues: BoardIssue[] }) {
78
+ export function IssuesBoardClient({
79
+ issues: initialIssues,
80
+ formatCtx = {},
81
+ }: {
82
+ issues: BoardIssue[];
83
+ formatCtx?: FormatContext;
84
+ }) {
74
85
  const [issues, setIssues] = useState<BoardIssue[]>(initialIssues);
75
86
  const [openId, setOpenId] = useState<string | null>(null);
76
87
  const [sourceFilter, setSourceFilter] = useState<IssueSource | 'all'>('all');
@@ -211,6 +222,7 @@ export function IssuesBoardClient({ issues: initialIssues }: { issues: BoardIssu
211
222
  {openIssue && (
212
223
  <IssueModal
213
224
  issue={openIssue}
225
+ formatCtx={formatCtx}
214
226
  onClose={() => setOpenId(null)}
215
227
  onSave={(updates) => updateIssue(openIssue.id, updates)}
216
228
  />
@@ -341,17 +353,7 @@ function Card({ issue, onOpen }: { issue: BoardIssue; onOpen: (id: string) => vo
341
353
  <h3 className="mt-1 text-body font-medium text-text-primary line-clamp-2">{issue.title}</h3>
342
354
  <div className="mt-2 flex flex-wrap items-center gap-1.5 text-caption">
343
355
  <SourceChip source={issue.source} />
344
- {issue.github_synced && (
345
- <a
346
- href={issue.github_issue_url}
347
- target="_blank"
348
- rel="noopener noreferrer"
349
- onClick={(e) => e.stopPropagation()}
350
- className="rounded-md bg-surface-2 px-1.5 py-0.5 text-text-secondary hover:bg-surface-3 hover:text-text-primary"
351
- >
352
- GH {issue.github_state === 'closed' ? '✕' : '✓'}
353
- </a>
354
- )}
356
+ <SyncChip issue={issue} />
355
357
  {issue.duplicate_of && (
356
358
  <span className="rounded-md bg-surface-2 px-1.5 py-0.5 text-text-secondary">
357
359
  dup of {issue.duplicate_of}
@@ -409,12 +411,60 @@ function SourceChip({ source }: { source: IssueSource }) {
409
411
  );
410
412
  }
411
413
 
414
+ /**
415
+ * GitHub-sync state chip. Only verified findings are push candidates, so other
416
+ * statuses render nothing (sync_state === 'none').
417
+ */
418
+ function SyncChip({ issue }: { issue: BoardIssue }) {
419
+ const state = issue.sync_state ?? 'none';
420
+ if (state === 'none') return null;
421
+
422
+ if (state === 'unsynced') {
423
+ return (
424
+ <span
425
+ title="Run /sync-issues to push verified findings to GitHub."
426
+ className="rounded-md border border-dashed border-border-default px-1.5 py-0.5 text-text-tertiary"
427
+ >
428
+ GH unsynced
429
+ </span>
430
+ );
431
+ }
432
+ if (state === 'conflict') {
433
+ return (
434
+ <span
435
+ title="Local and remote diverged. Re-run /sync-issues to reconcile."
436
+ className="rounded-md bg-[var(--critical)]/15 px-1.5 py-0.5 text-[var(--critical)]"
437
+ >
438
+ GH conflict
439
+ </span>
440
+ );
441
+ }
442
+ const closed = state === 'synced_closed';
443
+ const label = `GH #${issue.github_issue_number ?? '?'} ${closed ? 'closed' : 'open'}`;
444
+ const cls = closed ? 'bg-surface-2 text-text-tertiary' : 'bg-[var(--low)]/15 text-[var(--low)]';
445
+ return issue.github_issue_url ? (
446
+ <a
447
+ href={issue.github_issue_url}
448
+ target="_blank"
449
+ rel="noopener noreferrer"
450
+ onClick={(e) => e.stopPropagation()}
451
+ className={`rounded-md px-1.5 py-0.5 hover:underline ${cls}`}
452
+ >
453
+ {label}
454
+ </a>
455
+ ) : (
456
+ <span className={`rounded-md px-1.5 py-0.5 ${cls}`}>{label}</span>
457
+ );
458
+ }
459
+
412
460
  function IssueModal({
413
461
  issue,
462
+ formatCtx,
414
463
  onClose,
415
464
  onSave,
416
465
  }: {
417
466
  issue: BoardIssue;
467
+ formatCtx: FormatContext;
418
468
  onClose: () => void;
419
469
  onSave: (updates: Partial<BoardIssue>) => Promise<void> | void;
420
470
  }) {
@@ -425,12 +475,36 @@ function IssueModal({
425
475
  const [resolution, setResolution] = useState(issue.resolution ?? '');
426
476
  const [updateFromClient, setUpdateFromClient] = useState(issue.update_from_client ?? '');
427
477
  const [saving, setSaving] = useState(false);
478
+ const [copied, setCopied] = useState(false);
428
479
  const overlayRef = useRef<HTMLDivElement>(null);
429
480
 
430
481
  useEffect(() => {
431
482
  overlayRef.current?.focus();
432
483
  }, []);
433
484
 
485
+ // Copy the current (possibly edited) field values, not just the saved ones.
486
+ const copyAsHackmd = async () => {
487
+ const md = findingToHackmd(
488
+ {
489
+ ...issue,
490
+ title,
491
+ severity,
492
+ description,
493
+ recommendation,
494
+ resolution: resolution || undefined,
495
+ update_from_client: updateFromClient || undefined,
496
+ },
497
+ formatCtx,
498
+ );
499
+ try {
500
+ await navigator.clipboard.writeText(md);
501
+ setCopied(true);
502
+ setTimeout(() => setCopied(false), 1500);
503
+ } catch {
504
+ /* clipboard blocked (insecure context) — no-op */
505
+ }
506
+ };
507
+
434
508
  const submit = async () => {
435
509
  setSaving(true);
436
510
  const updates: Partial<BoardIssue> = {
@@ -466,19 +540,10 @@ function IssueModal({
466
540
  onClick={(e) => e.stopPropagation()}
467
541
  >
468
542
  <div className="flex items-center justify-between border-b border-border-default bg-surface-2 px-4 py-3">
469
- <div className="flex items-center gap-2">
543
+ <div className="flex items-center gap-2 text-caption">
470
544
  <span className="font-mono text-body text-text-tertiary">{issue.id}</span>
471
545
  <SourceChip source={issue.source} />
472
- {issue.github_synced && issue.github_issue_url && (
473
- <a
474
- href={issue.github_issue_url}
475
- target="_blank"
476
- rel="noopener noreferrer"
477
- className="rounded-md bg-surface-3 px-1.5 py-0.5 text-caption text-text-secondary hover:text-text-primary"
478
- >
479
- GitHub issue
480
- </a>
481
- )}
546
+ <SyncChip issue={issue} />
482
547
  </div>
483
548
  <button
484
549
  type="button"
@@ -596,22 +661,31 @@ function IssueModal({
596
661
  )}
597
662
  </div>
598
663
 
599
- <div className="flex items-center justify-end gap-2 border-t border-border-default bg-surface-2 px-4 py-3">
600
- <button
601
- type="button"
602
- onClick={onClose}
603
- className="rounded-md px-3 py-1.5 text-body text-text-secondary hover:text-text-primary"
604
- >
605
- Cancel
606
- </button>
664
+ <div className="flex items-center justify-between gap-2 border-t border-border-default bg-surface-2 px-4 py-3">
607
665
  <button
608
666
  type="button"
609
- onClick={submit}
610
- disabled={saving}
611
- className="rounded-md bg-accent px-3 py-1.5 text-body font-medium text-surface-0 hover:bg-accent-secondary disabled:opacity-50"
667
+ onClick={copyAsHackmd}
668
+ className="rounded-md border border-border-default px-3 py-1.5 text-body text-text-secondary hover:bg-surface-3 hover:text-text-primary"
612
669
  >
613
- {saving ? 'Saving…' : 'Save'}
670
+ {copied ? 'Copied!' : 'Copy as HackMD'}
614
671
  </button>
672
+ <div className="flex items-center gap-2">
673
+ <button
674
+ type="button"
675
+ onClick={onClose}
676
+ className="rounded-md px-3 py-1.5 text-body text-text-secondary hover:text-text-primary"
677
+ >
678
+ Cancel
679
+ </button>
680
+ <button
681
+ type="button"
682
+ onClick={submit}
683
+ disabled={saving}
684
+ className="rounded-md bg-accent px-3 py-1.5 text-body font-medium text-surface-0 hover:bg-accent-secondary disabled:opacity-50"
685
+ >
686
+ {saving ? 'Saving…' : 'Save'}
687
+ </button>
688
+ </div>
615
689
  </div>
616
690
  </div>
617
691
  </div>
@@ -8,7 +8,6 @@ import { IssuesBoardClient, type BoardIssue } from './IssuesBoardClient';
8
8
  // stale (or empty) HTML for every audit project, ignoring the runtime cwd.
9
9
  export const dynamic = 'force-dynamic';
10
10
 
11
-
12
11
  interface FindingsFile {
13
12
  findings: Array<{
14
13
  id: string;
@@ -20,11 +19,17 @@ interface FindingsFile {
20
19
  update_from_client?: string;
21
20
  root_cause?: { locations?: Array<{ file: string; snippet?: string }> };
22
21
  poc?: { status?: string; file?: string | null; validation_memo?: string | null };
23
- github?: { issue_number?: number; issue_url?: string; state?: string };
22
+ github?: { issue_number?: number; issue_url?: string; state?: string; sync_status?: string };
24
23
  category?: string;
25
24
  }>;
26
25
  }
27
26
 
27
+ interface ConfigFile {
28
+ settings?: {
29
+ report?: { repository_url?: string; initial_commit?: string };
30
+ };
31
+ }
32
+
28
33
  interface TrackingFile {
29
34
  findings: Array<{
30
35
  id: string;
@@ -74,6 +79,26 @@ interface SpecConformanceFile {
74
79
  }>;
75
80
  }
76
81
 
82
+ /**
83
+ * Derive the GitHub sync chip state. Only verified findings are push
84
+ * candidates, so anything else returns 'none' (no chip).
85
+ */
86
+ function computeSyncState(
87
+ status: string | undefined,
88
+ github: { issue_number?: number; state?: string; sync_status?: string } | undefined,
89
+ ): BoardIssue['sync_state'] {
90
+ if (status !== 'verified') return 'none';
91
+ if (!github?.issue_number) return 'unsynced';
92
+ if (
93
+ github.sync_status === 'conflict' ||
94
+ github.sync_status === 'local_ahead' ||
95
+ github.sync_status === 'remote_ahead'
96
+ ) {
97
+ return 'conflict';
98
+ }
99
+ return github.state === 'closed' ? 'synced_closed' : 'synced_open';
100
+ }
101
+
77
102
  function statusToColumn(status: string | undefined): BoardIssue['column'] {
78
103
  switch (status) {
79
104
  case 'verified':
@@ -179,7 +204,9 @@ function buildBoardIssues(): BoardIssue[] {
179
204
  code_locations: locations,
180
205
  github_synced: Boolean(finding?.github?.issue_number),
181
206
  github_issue_url: finding?.github?.issue_url,
207
+ github_issue_number: finding?.github?.issue_number,
182
208
  github_state: finding?.github?.state,
209
+ sync_state: computeSyncState(t.status, finding?.github),
183
210
  duplicate_of: t.duplicate_of ?? dup?.matches ?? null,
184
211
  match_signals: matchSignals,
185
212
  reasoning: dup?.reasoning,
@@ -210,7 +237,9 @@ function buildBoardIssues(): BoardIssue[] {
210
237
  code_locations: f.root_cause?.locations ?? [],
211
238
  github_synced: Boolean(f.github?.issue_number),
212
239
  github_issue_url: f.github?.issue_url,
240
+ github_issue_number: f.github?.issue_number,
213
241
  github_state: f.github?.state,
242
+ sync_state: computeSyncState('verified', f.github),
214
243
  duplicate_of: null,
215
244
  match_signals: undefined,
216
245
  reasoning: undefined,
@@ -227,6 +256,11 @@ function buildBoardIssues(): BoardIssue[] {
227
256
 
228
257
  export default function IssuesPage() {
229
258
  const issues = buildBoardIssues();
259
+ const config = readJsonFile<ConfigFile>('config.json');
260
+ const formatCtx = {
261
+ repositoryUrl: config?.settings?.report?.repository_url,
262
+ commit: config?.settings?.report?.initial_commit,
263
+ };
230
264
 
231
265
  if (issues.length === 0) {
232
266
  return (
@@ -242,5 +276,5 @@ export default function IssuesPage() {
242
276
  );
243
277
  }
244
278
 
245
- return <IssuesBoardClient issues={issues} />;
279
+ return <IssuesBoardClient issues={issues} formatCtx={formatCtx} />;
246
280
  }