@vintasoftware/pr-review-canvas 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +192 -0
  3. package/bin/pr-review.mjs +5 -0
  4. package/docs/reference.md +340 -0
  5. package/package.json +74 -0
  6. package/pr-review.config.example.yml +68 -0
  7. package/prompts/chat-seed.md +64 -0
  8. package/prompts/generation-format.md +255 -0
  9. package/prompts/generation-strict.md +34 -0
  10. package/prompts/generation-surfacing.md +67 -0
  11. package/prompts/layers-default.md +13 -0
  12. package/prompts/quality-standards.md +32 -0
  13. package/skills/pr-review-canvas/SKILL.md +177 -0
  14. package/src/acpx/acpx.ts +530 -0
  15. package/src/acpx/agents.ts +85 -0
  16. package/src/acpx/events.ts +216 -0
  17. package/src/acpx/ndjson.ts +69 -0
  18. package/src/acpx/preflight.ts +44 -0
  19. package/src/canvas/export.ts +95 -0
  20. package/src/canvas/import.ts +138 -0
  21. package/src/canvas/name.ts +55 -0
  22. package/src/canvas/zip.ts +123 -0
  23. package/src/chat/chat-manager.ts +389 -0
  24. package/src/chat/context.ts +160 -0
  25. package/src/chat/seed.ts +71 -0
  26. package/src/chat/threads.ts +114 -0
  27. package/src/cli.ts +199 -0
  28. package/src/commands.ts +424 -0
  29. package/src/config.ts +142 -0
  30. package/src/contract/api.ts +190 -0
  31. package/src/contract/canvas-manifest.ts +29 -0
  32. package/src/contract/chat.ts +76 -0
  33. package/src/contract/comments.ts +96 -0
  34. package/src/contract/discovery.ts +20 -0
  35. package/src/contract/generation-context.ts +77 -0
  36. package/src/contract/keys.ts +14 -0
  37. package/src/contract/links.ts +5 -0
  38. package/src/contract/mermaid-fences.ts +4 -0
  39. package/src/contract/review-artifact.ts +324 -0
  40. package/src/contract/settings.ts +144 -0
  41. package/src/contract/state.ts +46 -0
  42. package/src/contract/validation.ts +43 -0
  43. package/src/git/diff-collector.ts +151 -0
  44. package/src/git/git.ts +115 -0
  45. package/src/git/lang.ts +1 -0
  46. package/src/git/materialize.ts +79 -0
  47. package/src/git/patch-lines.ts +60 -0
  48. package/src/github/attachments.ts +288 -0
  49. package/src/github/capabilities.ts +112 -0
  50. package/src/github/comments.ts +132 -0
  51. package/src/github/gh.ts +196 -0
  52. package/src/github/post-comment.ts +104 -0
  53. package/src/github/post-review.ts +44 -0
  54. package/src/github/pr.ts +133 -0
  55. package/src/github/review-body.ts +72 -0
  56. package/src/github/threads.ts +63 -0
  57. package/src/paths.ts +10 -0
  58. package/src/project-config.ts +219 -0
  59. package/src/prompt-files.ts +26 -0
  60. package/src/review/diagram-nodes.ts +227 -0
  61. package/src/review/doctor.ts +139 -0
  62. package/src/review/glob.ts +33 -0
  63. package/src/review/install-skill.ts +107 -0
  64. package/src/review/normalize.ts +209 -0
  65. package/src/review/prepare.ts +165 -0
  66. package/src/review/prompt.ts +233 -0
  67. package/src/review/publish.ts +209 -0
  68. package/src/review/skill-command.ts +4 -0
  69. package/src/review/test-paths.ts +32 -0
  70. package/src/review/text-length.ts +15 -0
  71. package/src/review/trim-caps.ts +114 -0
  72. package/src/review/validate-folds.ts +110 -0
  73. package/src/review/validate.ts +520 -0
  74. package/src/server/app.ts +46 -0
  75. package/src/server/bundle.ts +266 -0
  76. package/src/server/capped-body.ts +62 -0
  77. package/src/server/context.ts +174 -0
  78. package/src/server/env.ts +7 -0
  79. package/src/server/errors.ts +65 -0
  80. package/src/server/html.ts +140 -0
  81. package/src/server/node-server.ts +42 -0
  82. package/src/server/routes/api.ts +256 -0
  83. package/src/server/routes/chat-routes.ts +221 -0
  84. package/src/server/routes/pages.ts +64 -0
  85. package/src/server/routes/review-routes.ts +245 -0
  86. package/src/server/routes/static.ts +114 -0
  87. package/src/server/security.ts +104 -0
  88. package/src/server/sse.ts +67 -0
  89. package/src/store/atomic-json.ts +68 -0
  90. package/src/store/canvas-store.ts +120 -0
  91. package/src/store/data-dir.ts +29 -0
  92. package/src/store/derived-store.ts +93 -0
  93. package/src/store/pr-store.ts +69 -0
  94. package/src/store/settings-store.ts +152 -0
  95. package/src/store/state-store.ts +121 -0
  96. package/static/js/anchors.js +141 -0
  97. package/static/js/api.js +542 -0
  98. package/static/js/app.js +418 -0
  99. package/static/js/ask.js +35 -0
  100. package/static/js/chat-context.js +137 -0
  101. package/static/js/chat-scroll.js +114 -0
  102. package/static/js/chat.js +843 -0
  103. package/static/js/code-folds.js +200 -0
  104. package/static/js/commands.js +110 -0
  105. package/static/js/comment-link.js +37 -0
  106. package/static/js/composer.js +241 -0
  107. package/static/js/contract-types.d.ts +59 -0
  108. package/static/js/deep-link.js +160 -0
  109. package/static/js/diagram.js +582 -0
  110. package/static/js/diff-decorations.js +204 -0
  111. package/static/js/diff-renderer.js +860 -0
  112. package/static/js/dom.js +145 -0
  113. package/static/js/download.js +52 -0
  114. package/static/js/empty-state.js +161 -0
  115. package/static/js/errors.js +135 -0
  116. package/static/js/fences.js +90 -0
  117. package/static/js/header.js +134 -0
  118. package/static/js/hunks.js +62 -0
  119. package/static/js/import-zone.js +95 -0
  120. package/static/js/interactions.js +952 -0
  121. package/static/js/keyboard.js +131 -0
  122. package/static/js/keys.js +97 -0
  123. package/static/js/lang.js +54 -0
  124. package/static/js/layers.js +596 -0
  125. package/static/js/links.js +150 -0
  126. package/static/js/markdown.js +232 -0
  127. package/static/js/mermaid-fences.js +55 -0
  128. package/static/js/nav.js +91 -0
  129. package/static/js/overview.js +85 -0
  130. package/static/js/points.js +247 -0
  131. package/static/js/progress.js +49 -0
  132. package/static/js/proposed-comment.js +133 -0
  133. package/static/js/quick-questions.js +216 -0
  134. package/static/js/regenerate.js +69 -0
  135. package/static/js/review-session.js +257 -0
  136. package/static/js/scroll-spy.js +66 -0
  137. package/static/js/selection.js +193 -0
  138. package/static/js/settings.js +206 -0
  139. package/static/js/signoff.js +171 -0
  140. package/static/js/skin.js +56 -0
  141. package/static/js/store.js +35 -0
  142. package/static/js/theme.js +56 -0
  143. package/static/js/threads.js +78 -0
  144. package/static/js/vendor.d.ts +15 -0
  145. package/static/styles/base.css +223 -0
  146. package/static/styles/chat-tools.css +130 -0
  147. package/static/styles/chat.css +140 -0
  148. package/static/styles/commands.css +156 -0
  149. package/static/styles/diff.css +258 -0
  150. package/static/styles/header.css +114 -0
  151. package/static/styles/layout.css +123 -0
  152. package/static/styles/panels.css +152 -0
  153. package/static/styles/responsive.css +80 -0
  154. package/static/styles/review-actions.css +124 -0
  155. package/static/styles/review.css +473 -0
  156. package/static/styles/skin-github.css +356 -0
  157. package/static/styles.css +14 -0
@@ -0,0 +1,131 @@
1
+ // @ts-check
2
+ // The key map of the review page. `keyAction` is the whole decision: which key does what, when
3
+ // the page is listening at all, and how the two-key `g o` sequence works.
4
+ import { esc } from './dom.js'
5
+
6
+ /**
7
+ * @typedef {'next-layer' | 'prev-layer' | 'next-file' | 'prev-file' | 'next-point' | 'prev-point'
8
+ * | 'toggle' | 'reviewed-file' | 'reviewed-layer' | 'comment' | 'dismiss' | 'overview'
9
+ * | 'help' | 'escape' | 'ask' | 'focus-chat'} KeyAction
10
+ */
11
+
12
+ export const KEY_HELP = [
13
+ { keys: 'j / k', what: 'next / previous layer' },
14
+ { keys: 'n / p', what: 'next / previous file' },
15
+ { keys: '] / [', what: 'next / previous attention point' },
16
+ { keys: 'o', what: 'open or collapse the card in focus' },
17
+ { keys: 'r', what: 'mark the file in focus reviewed' },
18
+ { keys: 'R', what: 'mark the layer in focus reviewed and move on' },
19
+ { keys: 'c', what: 'comment on the line in focus or on the selection' },
20
+ { keys: 'd', what: 'dismiss the attention point in focus' },
21
+ { keys: 'g o', what: 'go to the overview' },
22
+ { keys: '?', what: 'this help' },
23
+ { keys: 'Esc', what: 'clear the selection, close a composer or a dialog' },
24
+ { keys: 'a', what: 'ask AI Chat about the card, point, or selection in focus' },
25
+ { keys: '/', what: 'put the cursor in the AI Chat box' },
26
+ ]
27
+
28
+ /**
29
+ * True while the reader is typing, when no key of this map should fire.
30
+ * @param {EventTarget | null} target
31
+ */
32
+ export function isTypingTarget(target) {
33
+ if (!(target instanceof Element)) {
34
+ return false
35
+ }
36
+ const tag = target.tagName.toLowerCase()
37
+ return (
38
+ tag === 'input' || tag === 'textarea' || tag === 'select' || target.closest('[contenteditable="true"]') !== null
39
+ )
40
+ }
41
+
42
+ /**
43
+ * @param {KeyboardEvent} event
44
+ * @param {{ pendingG?: boolean }} [opts]
45
+ * @returns {{ action: KeyAction | null, pendingG: boolean }}
46
+ */
47
+ export function keyAction(event, opts = {}) {
48
+ const none = { action: null, pendingG: false }
49
+ if (event.ctrlKey || event.metaKey || event.altKey) {
50
+ return none
51
+ }
52
+ if (event.key === 'Escape') {
53
+ return { action: 'escape', pendingG: false }
54
+ }
55
+ if (isTypingTarget(event.target)) {
56
+ return none
57
+ }
58
+ if (opts.pendingG === true) {
59
+ return event.key === 'o' ? { action: 'overview', pendingG: false } : none
60
+ }
61
+ switch (event.key) {
62
+ case 'j':
63
+ return { action: 'next-layer', pendingG: false }
64
+ case 'k':
65
+ return { action: 'prev-layer', pendingG: false }
66
+ case 'n':
67
+ return { action: 'next-file', pendingG: false }
68
+ case 'p':
69
+ return { action: 'prev-file', pendingG: false }
70
+ case ']':
71
+ return { action: 'next-point', pendingG: false }
72
+ case '[':
73
+ return { action: 'prev-point', pendingG: false }
74
+ case 'o':
75
+ return { action: 'toggle', pendingG: false }
76
+ case 'r':
77
+ return { action: 'reviewed-file', pendingG: false }
78
+ case 'R':
79
+ return { action: 'reviewed-layer', pendingG: false }
80
+ case 'c':
81
+ return { action: 'comment', pendingG: false }
82
+ case 'd':
83
+ return { action: 'dismiss', pendingG: false }
84
+ case 'g':
85
+ return { action: null, pendingG: true }
86
+ case '?':
87
+ return { action: 'help', pendingG: false }
88
+ case 'a':
89
+ return { action: 'ask', pendingG: false }
90
+ case '/':
91
+ return { action: 'focus-chat', pendingG: false }
92
+ default:
93
+ return none
94
+ }
95
+ }
96
+
97
+ export const HELP_DIALOG_ID = 'help-dialog'
98
+
99
+ export function helpDialogHtml() {
100
+ const rows = KEY_HELP.map(r => `<tr><td class="mono">${esc(r.keys)}</td><td>${esc(r.what)}</td></tr>`).join('')
101
+ return (
102
+ `<dialog id="${HELP_DIALOG_ID}" class="help" aria-labelledby="help-h"><form method="dialog">` +
103
+ '<h2 id="help-h">Keyboard</h2>' +
104
+ `<table class="keys"><tbody>${rows}</tbody></table>` +
105
+ '<div class="dialog-actions"><button class="cmd" type="submit" value="close">close</button></div>' +
106
+ '</form></dialog>'
107
+ )
108
+ }
109
+
110
+ /**
111
+ * @param {HTMLElement} root
112
+ * @returns {HTMLDialogElement}
113
+ */
114
+ export function openHelpDialog(root) {
115
+ let dialog = root.querySelector(`#${HELP_DIALOG_ID}`)
116
+ if (!(dialog instanceof HTMLDialogElement)) {
117
+ const t = document.createElement('template')
118
+ t.innerHTML = helpDialogHtml()
119
+ root.appendChild(t.content)
120
+ dialog = root.querySelector(`#${HELP_DIALOG_ID}`)
121
+ }
122
+ if (!(dialog instanceof HTMLDialogElement)) {
123
+ throw new Error('help dialog did not render')
124
+ }
125
+ if (typeof dialog.showModal === 'function' && !dialog.open) {
126
+ dialog.showModal()
127
+ } else {
128
+ dialog.setAttribute('open', '')
129
+ }
130
+ return dialog
131
+ }
@@ -0,0 +1,97 @@
1
+ // @ts-check
2
+ // File keys and DOM anchor ids. One implementation for the server (re-exported by
3
+ // src/contract/keys.ts) and the browser, so hunk ids and line anchors never drift.
4
+
5
+ /**
6
+ * Turns a repo path into a key safe for ids and file names: every character that is not a
7
+ * letter or digit becomes "_". Two paths can collide (`a-b.ts` and `a_b.ts`); the diff collector
8
+ * adds a numeric suffix through `uniqueKey` when that happens.
9
+ * @param {string} path
10
+ * @returns {string}
11
+ */
12
+ export function sanitizeKey(path) {
13
+ return path.replace(/[^a-zA-Z0-9]/g, '_')
14
+ }
15
+
16
+ /**
17
+ * Returns `base` when unused, otherwise `base_2`, `base_3`, and so on.
18
+ * @param {string} base
19
+ * @param {Set<string>} used
20
+ * @returns {string}
21
+ */
22
+ export function uniqueKey(base, used) {
23
+ let key = base
24
+ let n = 2
25
+ while (used.has(key)) {
26
+ key = `${base}_${n}`
27
+ n += 1
28
+ }
29
+ used.add(key)
30
+ return key
31
+ }
32
+
33
+ /**
34
+ * @param {string} key
35
+ * @param {number} n 1-based position of the hunk in the file's patch
36
+ * @returns {string}
37
+ */
38
+ export function hunkId(key, n) {
39
+ return `${key}#${n}`
40
+ }
41
+
42
+ /**
43
+ * @param {string} id
44
+ * @returns {{ key: string, n: number } | null}
45
+ */
46
+ export function parseHunkId(id) {
47
+ const m = /^(.+)#(\d+)$/.exec(id)
48
+ if (!m || m[1] === undefined || m[2] === undefined) {
49
+ return null
50
+ }
51
+ return { key: m[1], n: Number(m[2]) }
52
+ }
53
+
54
+ /**
55
+ * @param {string} key
56
+ * @param {'new' | 'old'} side
57
+ * @param {number} line
58
+ * @returns {string}
59
+ */
60
+ export function buildLineId(key, side, line) {
61
+ return `L-${key}-${side}-${line}`
62
+ }
63
+
64
+ /**
65
+ * @param {string} id
66
+ * @returns {{ key: string, side: 'new' | 'old', line: number } | null}
67
+ */
68
+ export function parseLineId(id) {
69
+ const m = /^L-(.+)-(new|old)-(\d+)$/.exec(id)
70
+ if (!m || m[1] === undefined || m[3] === undefined) {
71
+ return null
72
+ }
73
+ return { key: m[1], side: m[2] === 'old' ? 'old' : 'new', line: Number(m[3]) }
74
+ }
75
+
76
+ /** @param {string} key */
77
+ export function fileAnchorId(key) {
78
+ return `file-${key}`
79
+ }
80
+
81
+ /**
82
+ * @param {string} key
83
+ * @param {number} n
84
+ */
85
+ export function hunkAnchorId(key, n) {
86
+ return `hunk-${key}-${n}`
87
+ }
88
+
89
+ /** @param {string} layerKey */
90
+ export function layerAnchorId(layerKey) {
91
+ return `layer-${layerKey}`
92
+ }
93
+
94
+ /** @param {string} pointId */
95
+ export function pointAnchorId(pointId) {
96
+ return `point-${pointId}`
97
+ }
@@ -0,0 +1,54 @@
1
+ // @ts-check
2
+ // highlight.js language per file extension. The server stores the result in `files[].lang`
3
+ // (src/git/lang.ts re-exports this) and the browser uses it to pick the grammar.
4
+
5
+ /** @type {Record<string, string>} */
6
+ export const LANG_BY_EXT = {
7
+ ts: 'typescript',
8
+ tsx: 'typescript',
9
+ mts: 'typescript',
10
+ cts: 'typescript',
11
+ js: 'javascript',
12
+ jsx: 'javascript',
13
+ mjs: 'javascript',
14
+ cjs: 'javascript',
15
+ json: 'json',
16
+ jsonc: 'json',
17
+ css: 'css',
18
+ scss: 'scss',
19
+ less: 'less',
20
+ md: 'markdown',
21
+ mdx: 'markdown',
22
+ yml: 'yaml',
23
+ yaml: 'yaml',
24
+ sh: 'bash',
25
+ bash: 'bash',
26
+ zsh: 'bash',
27
+ html: 'xml',
28
+ xml: 'xml',
29
+ svg: 'xml',
30
+ py: 'python',
31
+ rb: 'ruby',
32
+ go: 'go',
33
+ rs: 'rust',
34
+ java: 'java',
35
+ kt: 'kotlin',
36
+ sql: 'sql',
37
+ toml: 'ini',
38
+ ini: 'ini',
39
+ graphql: 'graphql',
40
+ gql: 'graphql',
41
+ prisma: 'prisma',
42
+ }
43
+
44
+ /**
45
+ * @param {string} path
46
+ * @returns {string | undefined}
47
+ */
48
+ export function langForPath(path) {
49
+ const m = /\.([a-zA-Z0-9]+)$/.exec(path)
50
+ if (!m || m[1] === undefined) {
51
+ return undefined
52
+ }
53
+ return LANG_BY_EXT[m[1].toLowerCase()]
54
+ }