scripts/review-artifacts.mjs
scripts/review-artifacts.mjsBrowse 11 files
4,331 tokens
17,854 bytes
Token encoding: o200k_base
Snapshot fac8604
← Back to SKILL.md
1const REVIEW_STATE_VERSION = 2;2const TARGET_KIND_VALUES = new Set([3 'review_thread',4 'review_comment',5 'pull_request_review',6 'issue_comment',7]);8 9function compareNullableStringsAsc(a, b) {10 const left = a ?? '';11 const right = b ?? '';12 if (left < right) return -1;13 if (left > right) return 1;14 return 0;15}16 17function compareNullableNumbersAsc(a, b) {18 const left = a ?? Number.MAX_SAFE_INTEGER;19 const right = b ?? Number.MAX_SAFE_INTEGER;20 return left - right;21}22 23function stripReviewFrameworkMarkers(body) {24 if (typeof body !== 'string') {25 return '';26 }27 return body28 .replace(/<!--\s*review-framework:[\s\S]*?-->/g, '')29 .replace(/<!--\s*internal state start\s*-->[\s\S]*?<!--\s*internal state end\s*-->/g, '')30 .replace(/\n{2,}/g, '\n')31 .replace(/[ \t]+\n/g, '\n')32 .trimEnd();33}34 35function normalizeReactionGroups(groups) {36 if (!Array.isArray(groups)) {37 return [];38 }39 const normalized = groups.map((group) => {40 const rawTotalCount = group?.users?.totalCount;41 const totalCount = Number.isFinite(rawTotalCount) ? Math.max(0, Math.trunc(rawTotalCount)) : 0;42 return {43 content: String(group?.content ?? ''),44 users: { totalCount },45 };46 });47 normalized.sort((a, b) => {48 if (a.content < b.content) return -1;49 if (a.content > b.content) return 1;50 return 0;51 });52 return normalized;53}54 55function earliestCommentCreatedAt(comments) {56 if (!Array.isArray(comments) || comments.length === 0) {57 return null;58 }59 let earliest = null;60 for (const comment of comments) {61 if (typeof comment?.createdAt === 'string' && comment.createdAt.length > 0) {62 if (earliest === null || comment.createdAt < earliest) {63 earliest = comment.createdAt;64 }65 }66 }67 return earliest;68}69 70function sortThreadComments(comments) {71 return [...comments].sort((a, b) => {72 const createdAtOrder = compareNullableStringsAsc(a.createdAt, b.createdAt);73 if (createdAtOrder !== 0) {74 return createdAtOrder;75 }76 return compareNullableStringsAsc(a.nodeId, b.nodeId);77 });78}79 80function sortReviewThreads(threads) {81 return [...threads].sort((a, b) => {82 const pathOrder = compareNullableStringsAsc(a.path, b.path);83 if (pathOrder !== 0) {84 return pathOrder;85 }86 const startLineOrder = compareNullableNumbersAsc(a.startLine, b.startLine);87 if (startLineOrder !== 0) {88 return startLineOrder;89 }90 const earliestOrder = compareNullableStringsAsc(91 earliestCommentCreatedAt(a.comments),92 earliestCommentCreatedAt(b.comments),93 );94 if (earliestOrder !== 0) {95 return earliestOrder;96 }97 return compareNullableStringsAsc(a.nodeId, b.nodeId);98 });99}100 101function sortReviews(reviews) {102 return [...reviews].sort((a, b) => {103 const submittedAtOrder = compareNullableStringsAsc(a.submittedAt, b.submittedAt);104 if (submittedAtOrder !== 0) {105 return submittedAtOrder;106 }107 return compareNullableStringsAsc(a.nodeId, b.nodeId);108 });109}110 111function sortIssueComments(comments) {112 return [...comments].sort((a, b) => {113 const createdAtOrder = compareNullableStringsAsc(a.createdAt, b.createdAt);114 if (createdAtOrder !== 0) {115 return createdAtOrder;116 }117 return compareNullableStringsAsc(a.nodeId, b.nodeId);118 });119}120 121function normalizeAuthor(author) {122 return {123 login: typeof author?.login === 'string' ? author.login : null,124 };125}126 127function normalizeBody(body) {128 return stripReviewFrameworkMarkers(body ?? '');129}130 131function normalizeThreadComment(comment) {132 if (typeof comment?.id !== 'string' || comment.id.length === 0) {133 return null;134 }135 return {136 nodeId: comment.id,137 url: typeof comment.url === 'string' ? comment.url : null,138 author: normalizeAuthor(comment.author),139 createdAt: typeof comment.createdAt === 'string' ? comment.createdAt : null,140 body: normalizeBody(comment.body),141 reactionGroups: normalizeReactionGroups(comment.reactionGroups),142 };143}144 145function summarizeBody(body, maxLength = 160) {146 if (typeof body !== 'string') {147 return '';148 }149 return body.replace(/\s+/g, ' ').trim().slice(0, maxLength);150}151 152function normalizeReview(review) {153 if (typeof review?.id !== 'string' || review.id.length === 0) {154 return null;155 }156 const body = normalizeBody(review.body);157 if (body.trim().length === 0) {158 return null;159 }160 return {161 nodeId: review.id,162 url: typeof review.url === 'string' ? review.url : null,163 author: normalizeAuthor(review.author),164 state: typeof review.state === 'string' ? review.state : null,165 submittedAt: typeof review.submittedAt === 'string' ? review.submittedAt : null,166 body,167 reactionGroups: normalizeReactionGroups(review.reactionGroups),168 };169}170 171function isActionableReview(review) {172 return review.state === 'CHANGES_REQUESTED' || review.state === 'COMMENTED';173}174 175function normalizeIssueComment(comment) {176 if (typeof comment?.id !== 'string' || comment.id.length === 0) {177 return null;178 }179 const body = normalizeBody(comment.body);180 if (body.trim().length === 0) {181 return null;182 }183 return {184 nodeId: comment.id,185 url: typeof comment.url === 'string' ? comment.url : null,186 author: normalizeAuthor(comment.author),187 createdAt: typeof comment.createdAt === 'string' ? comment.createdAt : null,188 body,189 reactionGroups: normalizeReactionGroups(comment.reactionGroups),190 replies: [],191 };192}193 194function normalizeReviewStateV2(input) {195 const normalizedThreads = [];196 const threadCandidates = Array.isArray(input?.reviewThreads) ? input.reviewThreads : [];197 for (const thread of threadCandidates) {198 if (thread?.isResolved !== false) continue;199 if (typeof thread?.id !== 'string' || thread.id.length === 0) continue;200 201 const normalizedComments = [];202 const commentCandidates = Array.isArray(thread?.comments?.nodes) ? thread.comments.nodes : [];203 for (const comment of commentCandidates) {204 const normalizedComment = normalizeThreadComment(comment);205 if (normalizedComment) normalizedComments.push(normalizedComment);206 }207 208 const sortedComments = sortThreadComments(normalizedComments);209 const primaryComment = sortedComments[0] ?? null;210 if (primaryComment === null) continue;211 const startLine =212 Number.isInteger(thread.startLine) && thread.startLine >= 0213 ? thread.startLine214 : Number.isInteger(thread.originalStartLine) && thread.originalStartLine >= 0215 ? thread.originalStartLine216 : null;217 const endLine =218 Number.isInteger(thread.line) && thread.line >= 0219 ? thread.line220 : Number.isInteger(thread.originalLine) && thread.originalLine >= 0221 ? thread.originalLine222 : null;223 224 normalizedThreads.push({225 threadKey: `review_thread:${thread.id}`,226 nodeId: thread.id,227 isResolved: false,228 isOutdated: Boolean(thread.isOutdated),229 path: typeof thread.path === 'string' ? thread.path : null,230 startLine,231 endLine,232 ordering: {233 path: typeof thread.path === 'string' ? thread.path : null,234 startLine,235 earliestCommentCreatedAt: earliestCommentCreatedAt(sortedComments),236 nodeId: thread.id,237 },238 primaryComment: {239 nodeId: primaryComment.nodeId,240 url: primaryComment.url,241 authorLogin: primaryComment.author.login,242 createdAt: primaryComment.createdAt,243 bodySnippet: summarizeBody(primaryComment.body),244 },245 targetHint: {246 kind: 'review_thread',247 nodeId: thread.id,248 url: primaryComment.url,249 },250 isActionableCandidate: !thread.isOutdated,251 comments: sortedComments,252 });253 }254 255 const normalizedReviews = [];256 const reviewCandidates = Array.isArray(input?.reviews) ? input.reviews : [];257 for (const review of reviewCandidates) {258 const normalizedReview = normalizeReview(review);259 if (normalizedReview) normalizedReviews.push(normalizedReview);260 }261 262 const normalizedIssueComments = [];263 const issueCommentCandidates = Array.isArray(input?.issueComments) ? input.issueComments : [];264 for (const issueComment of issueCommentCandidates) {265 const normalizedComment = normalizeIssueComment(issueComment);266 if (normalizedComment) normalizedIssueComments.push(normalizedComment);267 }268 269 const reviewThreads = sortReviewThreads(normalizedThreads);270 const sortedReviews = sortReviews(normalizedReviews);271 const sortedIssueComments = sortIssueComments(normalizedIssueComments);272 const threadTargets = reviewThreads.map((thread) => ({273 targetKey: thread.threadKey,274 kind: 'review_thread',275 nodeId: thread.nodeId,276 url: thread.targetHint.url,277 threadNodeId: thread.nodeId,278 path: thread.path,279 startLine: thread.startLine,280 endLine: thread.endLine,281 isOutdated: thread.isOutdated,282 isActionableCandidate: thread.isActionableCandidate,283 primaryCommentNodeId: thread.primaryComment?.nodeId ?? null,284 primaryCommentAuthorLogin: thread.primaryComment?.authorLogin ?? null,285 primaryCommentCreatedAt: thread.primaryComment?.createdAt ?? null,286 }));287 const reviewTargets = sortedReviews.map((review) => ({288 targetKey: `pull_request_review:${review.nodeId}`,289 kind: 'pull_request_review',290 nodeId: review.nodeId,291 url: review.url,292 path: null,293 startLine: null,294 endLine: null,295 isOutdated: false,296 isActionableCandidate: isActionableReview(review),297 primaryCommentNodeId: review.nodeId,298 primaryCommentAuthorLogin: review.author.login,299 primaryCommentCreatedAt: review.submittedAt,300 }));301 const issueCommentTargets = sortedIssueComments.map((comment) => ({302 targetKey: `issue_comment:${comment.nodeId}`,303 kind: 'issue_comment',304 nodeId: comment.nodeId,305 url: comment.url,306 path: null,307 startLine: null,308 endLine: null,309 isOutdated: false,310 isActionableCandidate: true,311 primaryCommentNodeId: comment.nodeId,312 primaryCommentAuthorLogin: comment.author.login,313 primaryCommentCreatedAt: comment.createdAt,314 }));315 316 return {317 version: REVIEW_STATE_VERSION,318 fetchedAt: String(input?.fetchedAt ?? ''),319 sourceBranch: typeof input?.sourceBranch === 'string' ? input.sourceBranch : null,320 pr: {321 url: typeof input?.pr?.url === 'string' ? input.pr.url : null,322 nodeId: typeof input?.pr?.id === 'string' ? input.pr.id : null,323 number: Number.isInteger(input?.pr?.number) ? input.pr.number : null,324 title: typeof input?.pr?.title === 'string' ? input.pr.title : null,325 state: typeof input?.pr?.state === 'string' ? input.pr.state : null,326 headRefName: typeof input?.pr?.headRefName === 'string' ? input.pr.headRefName : null,327 baseRefName: typeof input?.pr?.baseRefName === 'string' ? input.pr.baseRefName : null,328 updatedAt: typeof input?.pr?.updatedAt === 'string' ? input.pr.updatedAt : null,329 },330 reviewThreads,331 targets: [...threadTargets, ...reviewTargets, ...issueCommentTargets],332 reviews: sortedReviews,333 issueComments: sortedIssueComments,334 };335}336 337function isNonEmptyString(value) {338 return typeof value === 'string' && value.length > 0;339}340 341function validateReactionGroupShape(group, pointer) {342 if (!isNonEmptyString(group?.content)) {343 throw new TypeError(`${pointer}.content must be a non-empty string`);344 }345 if (!Number.isInteger(group?.users?.totalCount) || group.users.totalCount < 0) {346 throw new TypeError(`${pointer}.users.totalCount must be a non-negative integer`);347 }348}349 350function validateBodyEntryShape(entry, pointer) {351 if (!isNonEmptyString(entry?.nodeId)) {352 throw new TypeError(`${pointer}.nodeId must be a non-empty string`);353 }354 if (entry.url !== null && entry.url !== undefined && typeof entry.url !== 'string') {355 throw new TypeError(`${pointer}.url must be string or null`);356 }357 if (typeof entry?.author !== 'object' || entry.author === null) {358 throw new TypeError(`${pointer}.author must be an object`);359 }360 if (361 entry.author.login !== null &&362 entry.author.login !== undefined &&363 typeof entry.author.login !== 'string'364 ) {365 throw new TypeError(`${pointer}.author.login must be string or null`);366 }367 if (368 entry.createdAt !== null &&369 entry.createdAt !== undefined &&370 typeof entry.createdAt !== 'string'371 ) {372 throw new TypeError(`${pointer}.createdAt must be string or null`);373 }374 if (typeof entry.body !== 'string') {375 throw new TypeError(`${pointer}.body must be a string`);376 }377 if (!Array.isArray(entry.reactionGroups)) {378 throw new TypeError(`${pointer}.reactionGroups must be an array`);379 }380 for (let index = 0; index < entry.reactionGroups.length; index += 1) {381 validateReactionGroupShape(entry.reactionGroups[index], `${pointer}.reactionGroups[${index}]`);382 }383}384 385function validateReviewBodyShape(entry, pointer) {386 if (typeof entry !== 'object' || entry === null) {387 throw new TypeError(`${pointer} must be an object`);388 }389 if (!isNonEmptyString(entry.nodeId)) {390 throw new TypeError(`${pointer}.nodeId must be a non-empty string`);391 }392 if (typeof entry.author !== 'object' || entry.author === null) {393 throw new TypeError(`${pointer}.author must be an object`);394 }395 if (396 entry.author.login !== null &&397 entry.author.login !== undefined &&398 typeof entry.author.login !== 'string'399 ) {400 throw new TypeError(`${pointer}.author.login must be string or null`);401 }402 if (!isNonEmptyString(entry.body)) {403 throw new TypeError(`${pointer}.body must be a non-empty string`);404 }405 if (!Array.isArray(entry.reactionGroups)) {406 throw new TypeError(`${pointer}.reactionGroups must be an array`);407 }408 for (let index = 0; index < entry.reactionGroups.length; index += 1) {409 validateReactionGroupShape(entry.reactionGroups[index], `${pointer}.reactionGroups[${index}]`);410 }411}412 413function validateIssueCommentShape(entry, pointer) {414 validateReviewBodyShape(entry, pointer);415 if (!Array.isArray(entry.replies)) {416 throw new TypeError(`${pointer}.replies must be an array`);417 }418}419 420function assertReviewStateV2(reviewState) {421 if (typeof reviewState !== 'object' || reviewState === null) {422 throw new TypeError('review-state must be an object');423 }424 if (reviewState.version !== REVIEW_STATE_VERSION) {425 throw new TypeError(`review-state version must be ${REVIEW_STATE_VERSION}`);426 }427 if (!isNonEmptyString(reviewState.fetchedAt)) {428 throw new TypeError('review-state fetchedAt must be a non-empty string');429 }430 431 const pr = reviewState.pr;432 if (typeof pr !== 'object' || pr === null) {433 throw new TypeError('review-state pr must be an object');434 }435 if (!isNonEmptyString(pr.nodeId)) {436 throw new TypeError('review-state pr.nodeId must be a non-empty string');437 }438 if (!Array.isArray(reviewState.reviewThreads)) {439 throw new TypeError('review-state reviewThreads must be an array');440 }441 for (let index = 0; index < reviewState.reviewThreads.length; index += 1) {442 const thread = reviewState.reviewThreads[index];443 const pointer = `review-state reviewThreads[${index}]`;444 if (!isNonEmptyString(thread?.threadKey)) {445 throw new TypeError(`${pointer}.threadKey must be a non-empty string`);446 }447 if (!isNonEmptyString(thread?.nodeId)) {448 throw new TypeError(`${pointer}.nodeId must be a non-empty string`);449 }450 if (thread?.isResolved !== false) {451 throw new TypeError(`${pointer}.isResolved must be false`);452 }453 if (typeof thread?.ordering !== 'object' || thread.ordering === null) {454 throw new TypeError(`${pointer}.ordering must be an object`);455 }456 if (thread?.targetHint?.kind !== 'review_thread') {457 throw new TypeError(`${pointer}.targetHint.kind must be review_thread`);458 }459 if (!isNonEmptyString(thread?.targetHint?.nodeId)) {460 throw new TypeError(`${pointer}.targetHint.nodeId must be a non-empty string`);461 }462 if (typeof thread?.isActionableCandidate !== 'boolean') {463 throw new TypeError(`${pointer}.isActionableCandidate must be a boolean`);464 }465 if (!Array.isArray(thread.comments)) {466 throw new TypeError(`${pointer}.comments must be an array`);467 }468 for (let commentIndex = 0; commentIndex < thread.comments.length; commentIndex += 1) {469 validateBodyEntryShape(thread.comments[commentIndex], `${pointer}.comments[${commentIndex}]`);470 }471 }472 if (!Array.isArray(reviewState.reviews)) {473 throw new TypeError('review-state reviews must be an array');474 }475 for (let index = 0; index < reviewState.reviews.length; index += 1) {476 validateReviewBodyShape(reviewState.reviews[index], `review-state reviews[${index}]`);477 }478 if (!Array.isArray(reviewState.issueComments)) {479 throw new TypeError('review-state issueComments must be an array');480 }481 for (let index = 0; index < reviewState.issueComments.length; index += 1) {482 validateIssueCommentShape(483 reviewState.issueComments[index],484 `review-state issueComments[${index}]`,485 );486 }487 if (!Array.isArray(reviewState.targets)) {488 throw new TypeError('review-state targets must be an array');489 }490 for (let index = 0; index < reviewState.targets.length; index += 1) {491 const target = reviewState.targets[index];492 const pointer = `review-state targets[${index}]`;493 if (!isNonEmptyString(target?.targetKey)) {494 throw new TypeError(`${pointer}.targetKey must be a non-empty string`);495 }496 if (!TARGET_KIND_VALUES.has(target?.kind)) {497 throw new TypeError(`${pointer}.kind must be a supported target kind`);498 }499 if (!isNonEmptyString(target?.nodeId)) {500 throw new TypeError(`${pointer}.nodeId must be a non-empty string`);501 }502 }503 return reviewState;504}505 506function formatCanonicalJson(value) {507 return `${JSON.stringify(value, null, 2)}\n`;508}509 510export {511 assertReviewStateV2,512 formatCanonicalJson,513 normalizeReviewStateV2,514 REVIEW_STATE_VERSION,515 stripReviewFrameworkMarkers,516};517