@clearlydev/nx-archweave 0.2.1 → 0.3.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.
- package/README.md +9 -6
- package/dist/bin/review-verdict.js +69 -27
- package/dist/generators/add-agent-rules/generator.js +76 -931
- package/dist/generators/add-backend/generator.js +6 -931
- package/dist/generators/add-fsd-lite/generator.js +4 -929
- package/dist/generators/add-review/generator.js +368 -1113
- package/dist/generators/doctor/generator.js +488 -207
- package/dist/generators/init/generator.js +426 -1120
- package/package.json +4 -6
- package/src/generators/init/schema.json +6 -0
|
@@ -288,6 +288,30 @@ function buildOverEngineeringPrompt(locale) {
|
|
|
288
288
|
return [...body, "", renderFindingSchemaInstruction(locale, OVER_ENGINEERING_PRIORITIES), "", renderDedupInstruction(locale)].join("\n");
|
|
289
289
|
}
|
|
290
290
|
|
|
291
|
+
// ../review-policy/dist/verdict-gate/verdict-comment.js
|
|
292
|
+
var VERDICT_MARKER = "<!-- archweave-verdict -->";
|
|
293
|
+
function renderCommitLine(locale, shortSha) {
|
|
294
|
+
return locale === "en" ? `<sub>Commit \`${shortSha}\` \xB7 this comment is updated on every review round.</sub>` : `<sub>\uCEE4\uBC0B \`${shortSha}\` \xB7 \uC774 \uCF54\uBA58\uD2B8\uB294 \uB9AC\uBDF0 \uB77C\uC6B4\uB4DC\uB9C8\uB2E4 \uAC31\uC2E0\uB429\uB2C8\uB2E4.</sub>`;
|
|
295
|
+
}
|
|
296
|
+
var NOT_RUN_REASON = {
|
|
297
|
+
en: {
|
|
298
|
+
secret: (secretName) => `the ${secretName} secret is not registered`,
|
|
299
|
+
fork: () => "this pull request comes from a fork, and GitHub passes no secrets to runs from forks",
|
|
300
|
+
dependabot: (secretName) => `Dependabot runs receive only Dependabot secrets, and ${secretName} is not one of them`,
|
|
301
|
+
rounds: (_secretName, maxRounds) => `the automated review round limit (${maxRounds}) is reached`
|
|
302
|
+
},
|
|
303
|
+
ko: {
|
|
304
|
+
secret: (secretName) => `${secretName} \uC2DC\uD06C\uB9BF\uC774 \uB4F1\uB85D\uB418\uC9C0 \uC54A\uC74C`,
|
|
305
|
+
fork: () => "\uD3EC\uD06C\uC5D0\uC11C \uC628 pull request\uC774\uACE0, GitHub\uB294 \uD3EC\uD06C \uC2E4\uD589\uC5D0 \uC2DC\uD06C\uB9BF\uC744 \uB118\uAE30\uC9C0 \uC54A\uC74C",
|
|
306
|
+
dependabot: (secretName) => `Dependabot \uC2E4\uD589\uC740 Dependabot \uC2DC\uD06C\uB9BF\uB9CC \uBC1B\uB294\uB370, ${secretName}\uB294 \uADF8\uC911\uC5D0 \uC5C6\uC74C`,
|
|
307
|
+
rounds: (_secretName, maxRounds) => `\uC790\uB3D9 \uB9AC\uBDF0 \uB77C\uC6B4\uB4DC \uD55C\uB3C4(${maxRounds})\uC5D0 \uB3C4\uB2EC\uD568`
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
function renderNotRunNotice(locale, reason, secretName, maxRounds) {
|
|
311
|
+
const why = NOT_RUN_REASON[locale][reason](secretName, maxRounds);
|
|
312
|
+
return locale === "en" ? `AI review did not run for this commit (${why}). A green check here does not mean the change was reviewed.` : `\uC774 \uCEE4\uBC0B\uC5D0\uB294 AI \uB9AC\uBDF0\uAC00 \uC2E4\uD589\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4(${why}). \uC774 green \uCCB4\uD06C\uB294 \uBCC0\uACBD \uC0AC\uD56D\uC774 \uB9AC\uBDF0\uB418\uC5C8\uB2E4\uB294 \uB73B\uC774 \uC544\uB2D9\uB2C8\uB2E4.`;
|
|
313
|
+
}
|
|
314
|
+
|
|
291
315
|
// ../review-policy/dist/verdict-gate/verdict-gate.js
|
|
292
316
|
var ROUND_MARKER = "<!-- archweave-review-round -->";
|
|
293
317
|
|
|
@@ -320,13 +344,14 @@ function renderConcurrencyBlock() {
|
|
|
320
344
|
" cancel-in-progress: false"
|
|
321
345
|
].join("\n");
|
|
322
346
|
}
|
|
323
|
-
|
|
324
|
-
|
|
347
|
+
var REVIEW_GATE_CONDITION = "steps.secret-gate.outputs.enabled == 'true' && steps.round-gate.outputs.allowed == 'true'";
|
|
348
|
+
function renderReviewPermissions(idToken) {
|
|
349
|
+
return ["permissions:", " contents: read", " pull-requests: write", ...idToken ? [" id-token: write"] : []].join("\n");
|
|
325
350
|
}
|
|
326
351
|
function renderCheckoutStep(gateCondition, extraWith = []) {
|
|
327
352
|
return [
|
|
328
353
|
"- name: Checkout repository",
|
|
329
|
-
` if: ${gateCondition}
|
|
354
|
+
...gateCondition === void 0 ? [] : [` if: ${gateCondition}`],
|
|
330
355
|
` uses: ${pinnedUses("actions/checkout")}`,
|
|
331
356
|
" with:",
|
|
332
357
|
...extraWith.map((line) => ` ${line}`),
|
|
@@ -368,10 +393,84 @@ function renderRoundGateStep(maxRounds) {
|
|
|
368
393
|
" fi"
|
|
369
394
|
].join("\n");
|
|
370
395
|
}
|
|
396
|
+
function shellQuote(text) {
|
|
397
|
+
return `'${text.replace(/'/g, `'\\''`)}'`;
|
|
398
|
+
}
|
|
399
|
+
function renderNotRunFunction(secretName, maxRounds, locale) {
|
|
400
|
+
const reasons = ["secret", "fork", "dependabot", "rounds"];
|
|
401
|
+
const [commitBefore, commitAfter] = renderCommitLine(locale, "\0").split("\0");
|
|
402
|
+
return [
|
|
403
|
+
"not_run() {",
|
|
404
|
+
' case "$1" in',
|
|
405
|
+
...reasons.map((reason) => ` ${reason}) annotation=${shellQuote(renderNotRunNotice("en", reason, secretName, maxRounds))}; notice=${shellQuote(renderNotRunNotice(locale, reason, secretName, maxRounds))} ;;`),
|
|
406
|
+
" esac",
|
|
407
|
+
' echo "::notice title=AI review did not run::$annotation"',
|
|
408
|
+
` printf '%s\\n' "$notice" >> "$GITHUB_STEP_SUMMARY"`,
|
|
409
|
+
` short_sha=$(printf '%s' "$HEAD_SHA" | cut -c1-7)`,
|
|
410
|
+
' case "$short_sha" in',
|
|
411
|
+
" '' | *[!0-9a-f]*) short_sha= ;;",
|
|
412
|
+
" esac",
|
|
413
|
+
" {",
|
|
414
|
+
' echo "notice-body<<ARCHWEAVE_NOT_RUN_EOF"',
|
|
415
|
+
` printf '%s\\n' ${shellQuote(VERDICT_MARKER)} "\u2139\uFE0F $notice"`,
|
|
416
|
+
` if [ -n "$short_sha" ]; then printf '\\n%s\\n' ${shellQuote(commitBefore)}"$short_sha"${shellQuote(commitAfter)}; fi`,
|
|
417
|
+
' echo "ARCHWEAVE_NOT_RUN_EOF"',
|
|
418
|
+
' echo "run=false"',
|
|
419
|
+
' } >> "$GITHUB_OUTPUT"',
|
|
420
|
+
"}"
|
|
421
|
+
];
|
|
422
|
+
}
|
|
423
|
+
var JUDGE_CONDITION = "steps.ai-review-result.outputs.run == 'true'";
|
|
424
|
+
function renderAiReviewResultStep(secretName, maxRounds, locale) {
|
|
425
|
+
return [
|
|
426
|
+
"- name: Check the AI review result",
|
|
427
|
+
" id: ai-review-result",
|
|
428
|
+
" env:",
|
|
429
|
+
" AI_REVIEW_RESULT: ${{ needs.ai-review.result }}",
|
|
430
|
+
" REVIEW_ENABLED: ${{ needs.ai-review.outputs.enabled }}",
|
|
431
|
+
" REVIEW_ALLOWED: ${{ needs.ai-review.outputs.allowed }}",
|
|
432
|
+
" LAST_COMMENT_ID: ${{ needs.ai-review.outputs.last-comment-id }}",
|
|
433
|
+
" HEAD_SHA: ${{ github.event.pull_request.head.sha }}",
|
|
434
|
+
" HEAD_REPOSITORY: ${{ github.event.pull_request.head.repo.full_name }}",
|
|
435
|
+
" REPOSITORY: ${{ github.repository }}",
|
|
436
|
+
" PR_AUTHOR: ${{ github.event.pull_request.user.login }}",
|
|
437
|
+
" run: |",
|
|
438
|
+
...renderNotRunFunction(secretName, maxRounds, locale).map((line) => ` ${line}`),
|
|
439
|
+
' if [ "$AI_REVIEW_RESULT" != success ]; then',
|
|
440
|
+
` echo "::error title=AI review did not succeed::The AI review job ended with '$AI_REVIEW_RESULT'."`,
|
|
441
|
+
" exit 1",
|
|
442
|
+
" fi",
|
|
443
|
+
' case "$REVIEW_ENABLED/$REVIEW_ALLOWED" in',
|
|
444
|
+
" false/*)",
|
|
445
|
+
' if [ "$HEAD_REPOSITORY" != "$REPOSITORY" ]; then not_run fork',
|
|
446
|
+
` elif [ "$PR_AUTHOR" = 'dependabot[bot]' ]; then not_run dependabot`,
|
|
447
|
+
" else not_run secret",
|
|
448
|
+
" fi",
|
|
449
|
+
" exit 0",
|
|
450
|
+
" ;;",
|
|
451
|
+
" true/false)",
|
|
452
|
+
" not_run rounds",
|
|
453
|
+
" exit 0",
|
|
454
|
+
" ;;",
|
|
455
|
+
" true/true)",
|
|
456
|
+
' case "$LAST_COMMENT_ID" in',
|
|
457
|
+
" '' | *[!0-9]*) ;;",
|
|
458
|
+
" *)",
|
|
459
|
+
' echo "run=true" >> "$GITHUB_OUTPUT"',
|
|
460
|
+
" exit 0",
|
|
461
|
+
" ;;",
|
|
462
|
+
" esac",
|
|
463
|
+
" ;;",
|
|
464
|
+
" esac",
|
|
465
|
+
` echo "::error title=Review verdict not computed::The AI review job's results did not reach this job, so no verdict can be computed. Re-run all jobs."`,
|
|
466
|
+
" exit 1"
|
|
467
|
+
].join("\n");
|
|
468
|
+
}
|
|
371
469
|
function renderVerdictGateStep(maxRounds, locale) {
|
|
372
470
|
return [
|
|
373
471
|
"- name: Compute review verdict",
|
|
374
|
-
"
|
|
472
|
+
" id: verdict",
|
|
473
|
+
` if: ${JUDGE_CONDITION}`,
|
|
375
474
|
" env:",
|
|
376
475
|
" GH_TOKEN: ${{ github.token }}",
|
|
377
476
|
" GITHUB_REPOSITORY: ${{ github.repository }}",
|
|
@@ -379,7 +478,8 @@ function renderVerdictGateStep(maxRounds, locale) {
|
|
|
379
478
|
` ARCHWEAVE_MAX_ROUNDS: '${maxRounds}'`,
|
|
380
479
|
` ARCHWEAVE_LOCALE: '${locale}'`,
|
|
381
480
|
" ARCHWEAVE_HEAD_SHA: ${{ github.event.pull_request.head.sha }}",
|
|
382
|
-
" ARCHWEAVE_ROUND_AFTER_ID: ${{
|
|
481
|
+
" ARCHWEAVE_ROUND_AFTER_ID: ${{ needs.ai-review.outputs.last-comment-id }}",
|
|
482
|
+
" ARCHWEAVE_VERDICT_OUTPUT: 'github-output'",
|
|
383
483
|
" run: |",
|
|
384
484
|
" if [ -x node_modules/.bin/archweave-review-verdict ]; then",
|
|
385
485
|
" node_modules/.bin/archweave-review-verdict",
|
|
@@ -388,16 +488,49 @@ function renderVerdictGateStep(maxRounds, locale) {
|
|
|
388
488
|
" fi"
|
|
389
489
|
].join("\n");
|
|
390
490
|
}
|
|
491
|
+
function renderBlockStep() {
|
|
492
|
+
return [
|
|
493
|
+
"- name: Fail on a blocking verdict",
|
|
494
|
+
" if: steps.verdict.outputs.blocked == 'true'",
|
|
495
|
+
" run: |",
|
|
496
|
+
' echo "The review check failed: the verdict is BLOCK, or the AI review left no readable summary. See the verdict comment on this pull request."',
|
|
497
|
+
" exit 1"
|
|
498
|
+
].join("\n");
|
|
499
|
+
}
|
|
500
|
+
function renderVerdictCommentStep() {
|
|
501
|
+
return [
|
|
502
|
+
"- name: Post review verdict",
|
|
503
|
+
" env:",
|
|
504
|
+
" GH_TOKEN: ${{ github.token }}",
|
|
505
|
+
" REPOSITORY: ${{ github.repository }}",
|
|
506
|
+
" PR_NUMBER: ${{ github.event.pull_request.number }}",
|
|
507
|
+
" VERDICT_BODY: ${{ needs.review.outputs.comment-body }}",
|
|
508
|
+
" PREVIOUS_ID: ${{ needs.review.outputs.previous-comment-id }}",
|
|
509
|
+
" run: |",
|
|
510
|
+
` payload=$(jq -n --arg body "$VERDICT_BODY" '{body: $body}')`,
|
|
511
|
+
' if [ -z "$PREVIOUS_ID" ]; then',
|
|
512
|
+
` PREVIOUS_ID=$(gh api "repos/$REPOSITORY/issues/$PR_NUMBER/comments" --paginate --jq '.[] | select(.user.login == "github-actions[bot]") | select(.body | contains("${VERDICT_MARKER}")) | .id' | tail -n 1) || PREVIOUS_ID=`,
|
|
513
|
+
" fi",
|
|
514
|
+
' case "$PREVIOUS_ID" in',
|
|
515
|
+
" ''|*[!0-9]*) PREVIOUS_ID= ;;",
|
|
516
|
+
" esac",
|
|
517
|
+
` if [ -n "$PREVIOUS_ID" ] && printf '%s' "$payload" | gh api -X PATCH "repos/$REPOSITORY/issues/comments/$PREVIOUS_ID" --input - > /dev/null; then`,
|
|
518
|
+
" exit 0",
|
|
519
|
+
" fi",
|
|
520
|
+
` printf '%s' "$payload" | gh api -X POST "repos/$REPOSITORY/issues/$PR_NUMBER/comments" --input - > /dev/null \\`,
|
|
521
|
+
' || echo "Could not post the verdict comment (continuing)."'
|
|
522
|
+
].join("\n");
|
|
523
|
+
}
|
|
391
524
|
var SETUP_NODE_VERSION = "22";
|
|
392
525
|
var FALLBACK_PNPM_VERSION = "10";
|
|
393
526
|
var INSTALL_LINES = {
|
|
394
|
-
pnpm: [" pnpm install --frozen-lockfile"],
|
|
395
|
-
npm: [" npm ci"],
|
|
527
|
+
pnpm: [" pnpm install --frozen-lockfile --ignore-scripts"],
|
|
528
|
+
npm: [" npm ci --ignore-scripts"],
|
|
396
529
|
yarn: [
|
|
397
530
|
" corepack enable yarn",
|
|
398
|
-
' if [ "$(yarn --version | cut -d. -f1)" -ge 2 ]; then yarn install --immutable; else yarn install --frozen-lockfile; fi'
|
|
531
|
+
' if [ "$(yarn --version | cut -d. -f1)" -ge 2 ]; then YARN_ENABLE_SCRIPTS=false yarn install --immutable --mode=skip-build; else yarn install --frozen-lockfile --ignore-scripts; fi'
|
|
399
532
|
],
|
|
400
|
-
bun: [" bun install --frozen-lockfile"]
|
|
533
|
+
bun: [" bun install --frozen-lockfile --ignore-scripts"]
|
|
401
534
|
};
|
|
402
535
|
function renderPnpmVersionStep(gate) {
|
|
403
536
|
return [
|
|
@@ -413,7 +546,7 @@ function renderPnpmVersionStep(gate) {
|
|
|
413
546
|
].join("\n");
|
|
414
547
|
}
|
|
415
548
|
function renderWorkspaceSetupSteps(packageManager) {
|
|
416
|
-
const gate =
|
|
549
|
+
const gate = ` if: ${JUDGE_CONDITION}`;
|
|
417
550
|
const steps = [];
|
|
418
551
|
if (packageManager === "pnpm") {
|
|
419
552
|
steps.push(renderPnpmVersionStep(gate));
|
|
@@ -448,21 +581,89 @@ function renderWorkspaceSetupSteps(packageManager) {
|
|
|
448
581
|
].join("\n"));
|
|
449
582
|
return steps.join("\n\n");
|
|
450
583
|
}
|
|
584
|
+
var REVIEW_JOB_TIMEOUT_MINUTES = 15;
|
|
585
|
+
function renderReviewWorkflow(input, parts) {
|
|
586
|
+
const aiReviewSteps = [renderSecretGateStep(parts.secretName), renderRoundGateStep(input.maxRounds), ...parts.reviewSteps];
|
|
587
|
+
const reviewSteps = [
|
|
588
|
+
renderAiReviewResultStep(parts.secretName, input.maxRounds, input.locale),
|
|
589
|
+
renderCheckoutStep(JUDGE_CONDITION),
|
|
590
|
+
renderWorkspaceSetupSteps(input.packageManager),
|
|
591
|
+
renderVerdictGateStep(input.maxRounds, input.locale),
|
|
592
|
+
renderBlockStep()
|
|
593
|
+
];
|
|
594
|
+
return [
|
|
595
|
+
`name: ${parts.name}`,
|
|
596
|
+
"",
|
|
597
|
+
renderTriggerBlock(),
|
|
598
|
+
"",
|
|
599
|
+
renderConcurrencyBlock(),
|
|
600
|
+
"",
|
|
601
|
+
"permissions: {}",
|
|
602
|
+
"",
|
|
603
|
+
"jobs:",
|
|
604
|
+
" ai-review:",
|
|
605
|
+
" runs-on: ubuntu-latest",
|
|
606
|
+
` timeout-minutes: ${REVIEW_JOB_TIMEOUT_MINUTES}`,
|
|
607
|
+
indentBlock(renderReviewPermissions(parts.idToken), 4),
|
|
608
|
+
" outputs:",
|
|
609
|
+
" enabled: ${{ steps.secret-gate.outputs.enabled }}",
|
|
610
|
+
" allowed: ${{ steps.round-gate.outputs.allowed }}",
|
|
611
|
+
" last-comment-id: ${{ steps.round-gate.outputs.last-comment-id }}",
|
|
612
|
+
" steps:",
|
|
613
|
+
indentBlock(aiReviewSteps.join("\n\n"), 6),
|
|
614
|
+
"",
|
|
615
|
+
" review:",
|
|
616
|
+
" needs: ai-review",
|
|
617
|
+
" if: always()",
|
|
618
|
+
" runs-on: ubuntu-latest",
|
|
619
|
+
` timeout-minutes: ${REVIEW_JOB_TIMEOUT_MINUTES}`,
|
|
620
|
+
" permissions:",
|
|
621
|
+
" contents: read",
|
|
622
|
+
" pull-requests: read",
|
|
623
|
+
" outputs:",
|
|
624
|
+
" comment-body: ${{ steps.verdict.outputs.comment-body || steps.ai-review-result.outputs.notice-body }}",
|
|
625
|
+
" previous-comment-id: ${{ steps.verdict.outputs.previous-comment-id }}",
|
|
626
|
+
" steps:",
|
|
627
|
+
indentBlock(reviewSteps.join("\n\n"), 6),
|
|
628
|
+
"",
|
|
629
|
+
" review-comment:",
|
|
630
|
+
" needs: review",
|
|
631
|
+
// Runs after a BLOCK failed the review job, too: the comment explains the red check.
|
|
632
|
+
" if: ${{ !cancelled() && needs.review.outputs.comment-body != '' }}",
|
|
633
|
+
" runs-on: ubuntu-latest",
|
|
634
|
+
` timeout-minutes: ${REVIEW_JOB_TIMEOUT_MINUTES}`,
|
|
635
|
+
" permissions:",
|
|
636
|
+
" pull-requests: write",
|
|
637
|
+
" steps:",
|
|
638
|
+
indentBlock(renderVerdictCommentStep(), 6),
|
|
639
|
+
""
|
|
640
|
+
].join("\n");
|
|
641
|
+
}
|
|
451
642
|
|
|
452
643
|
// ../template-engine/dist/review/claude/claude-provider.js
|
|
453
644
|
var SECRET_NAME = "CLAUDE_CODE_OAUTH_TOKEN";
|
|
454
645
|
var ALLOWED_TOOLS = "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*)";
|
|
646
|
+
var CLAUDE_STEP_ID = "claude-review";
|
|
647
|
+
var PR_CONTEXT = ["REPO: ${{ github.repository }}", "PR NUMBER: ${{ github.event.pull_request.number }}"];
|
|
455
648
|
function renderWorkflowInstructions(locale) {
|
|
456
649
|
const lines = locale === "en" ? [
|
|
457
|
-
|
|
458
|
-
"
|
|
650
|
+
...PR_CONTEXT,
|
|
651
|
+
"",
|
|
652
|
+
"Review the diff of this pull request using `gh pr diff <PR NUMBER>` and `gh pr view <PR NUMBER>`.",
|
|
653
|
+
"Post specific findings as inline comments with `mcp__github_inline_comment__create_inline_comment` (pass `confirmed: true`), and exactly one top-level summary comment.",
|
|
654
|
+
"Post the summary with `gh pr comment <PR NUMBER> --body-file - <<'ARCHWEAVE_EOF'`, the body on the following lines, then a line `ARCHWEAVE_EOF` (the quoted delimiter keeps backticks and `$` literal). Never pass the body with `--body \"...\"` or `$(...)`.",
|
|
655
|
+
"Run each `gh` command on its own: no pipes to other programs, no `&&` chains.",
|
|
459
656
|
`Always include the literal marker ${ROUND_MARKER} in the top-level summary comment.`,
|
|
460
657
|
"If there are no findings that require action, post exactly one top-level comment saying so (and still include the marker).",
|
|
461
658
|
"Do not modify any code, and never approve or merge the pull request.",
|
|
462
659
|
"Write summaries and explanations in English; keep file names, function names, and identifiers verbatim."
|
|
463
660
|
] : [
|
|
464
|
-
|
|
465
|
-
"
|
|
661
|
+
...PR_CONTEXT,
|
|
662
|
+
"",
|
|
663
|
+
"\uC774 pull request\uC758 diff\uB97C `gh pr diff <PR NUMBER>`\uC640 `gh pr view <PR NUMBER>`\uB85C \uD655\uC778\uD558\uACE0 \uB9AC\uBDF0\uD558\uC138\uC694.",
|
|
664
|
+
"\uAD6C\uCCB4\uC801 \uC9C0\uC801\uC740 `mcp__github_inline_comment__create_inline_comment`(`confirmed: true` \uC804\uB2EC)\uB85C inline comment\uB97C \uB2EC\uACE0, \uC804\uCCB4 \uC694\uC57D\uC740 \uC815\uD655\uD788 \uD55C \uAC1C\uC758 top-level comment\uB85C \uAC8C\uC2DC\uD558\uC138\uC694.",
|
|
665
|
+
"\uC694\uC57D\uC740 `gh pr comment <PR NUMBER> --body-file - <<'ARCHWEAVE_EOF'`\uB85C \uAC8C\uC2DC\uD558\uC138\uC694. \uB2E4\uC74C \uC904\uBD80\uD130 \uBCF8\uBB38\uC744 \uC4F0\uACE0, \uB9C8\uC9C0\uB9C9 \uC904\uC5D0 `ARCHWEAVE_EOF`\uB9CC \uC4F0\uC138\uC694(\uB530\uC634\uD45C \uCE5C \uAD6C\uBD84\uC790\uB77C \uBC31\uD2F1\uACFC `$`\uAC00 \uADF8\uB300\uB85C \uB0A8\uC544\uC694). \uBCF8\uBB38\uC744 `--body \"...\"`\uB098 `$(...)`\uB85C \uB118\uAE30\uC9C0 \uB9C8\uC138\uC694.",
|
|
666
|
+
"`gh` \uBA85\uB839\uC740 \uD558\uB098\uC529 \uB530\uB85C \uC2E4\uD589\uD558\uC138\uC694. \uB2E4\uB978 \uD504\uB85C\uADF8\uB7A8\uC73C\uB85C \uD30C\uC774\uD504\uD558\uAC70\uB098 `&&`\uB85C \uC787\uC9C0 \uB9C8\uC138\uC694.",
|
|
466
667
|
`top-level \uC694\uC57D \uCF54\uBA58\uD2B8\uC5D0\uB294 \uBC18\uB4DC\uC2DC \uB9AC\uD130\uB7F4 \uB9C8\uCEE4 ${ROUND_MARKER}\uB97C \uD3EC\uD568\uD558\uC138\uC694.`,
|
|
467
668
|
'\uC870\uCE58\uAC00 \uD544\uC694\uD55C \uBB38\uC81C\uAC00 \uC5C6\uC73C\uBA74 "\uC870\uCE58\uAC00 \uD544\uC694\uD55C \uBB38\uC81C\uB97C \uCC3E\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4."\uB77C\uB294 top-level comment\uB97C \uC815\uD655\uD788 \uD55C \uAC1C \uAC8C\uC2DC\uD558\uC138\uC694(\uC774\uB54C\uB3C4 \uB9C8\uCEE4\uB294 \uD3EC\uD568).',
|
|
468
669
|
"\uCF54\uB4DC\uB97C \uC218\uC815\uD558\uC9C0 \uB9C8\uC138\uC694(Do not modify any code). PR\uC744 \uC2B9\uC778\uD558\uAC70\uB098 \uBCD1\uD569\uD558\uC9C0 \uB9C8\uC138\uC694(never approve or merge).",
|
|
@@ -479,42 +680,44 @@ function composePrompt(input) {
|
|
|
479
680
|
sections.push(renderFindingsBlockInstruction(input.locale));
|
|
480
681
|
return sections.join("\n\n---\n\n");
|
|
481
682
|
}
|
|
482
|
-
function
|
|
483
|
-
const steps = [
|
|
484
|
-
renderSecretGateStep(SECRET_NAME),
|
|
485
|
-
renderRoundGateStep(input.maxRounds),
|
|
486
|
-
renderCheckoutStep("steps.secret-gate.outputs.enabled == 'true' && steps.round-gate.outputs.allowed == 'true'"),
|
|
487
|
-
[
|
|
488
|
-
"- name: Run Claude review",
|
|
489
|
-
" if: steps.secret-gate.outputs.enabled == 'true' && steps.round-gate.outputs.allowed == 'true'",
|
|
490
|
-
` uses: ${pinnedUses("anthropics/claude-code-action")}`,
|
|
491
|
-
" with:",
|
|
492
|
-
` claude_code_oauth_token: \${{ secrets.${SECRET_NAME} }}`,
|
|
493
|
-
" prompt: |",
|
|
494
|
-
indentBlock(composePrompt(input), 6),
|
|
495
|
-
" claude_args: |",
|
|
496
|
-
` --allowedTools "${ALLOWED_TOOLS}"`
|
|
497
|
-
].join("\n"),
|
|
498
|
-
renderWorkspaceSetupSteps(input.packageManager),
|
|
499
|
-
renderVerdictGateStep(input.maxRounds, input.locale)
|
|
500
|
-
];
|
|
683
|
+
function renderDeniedToolsStep() {
|
|
501
684
|
return [
|
|
502
|
-
"name:
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
"",
|
|
508
|
-
|
|
509
|
-
"",
|
|
510
|
-
"
|
|
511
|
-
"
|
|
512
|
-
" runs-on: ubuntu-latest",
|
|
513
|
-
" steps:",
|
|
514
|
-
indentBlock(steps.join("\n\n"), 6),
|
|
515
|
-
""
|
|
685
|
+
"- name: Show denied tool calls",
|
|
686
|
+
` if: always() && steps.${CLAUDE_STEP_ID}.outputs.execution_file != ''`,
|
|
687
|
+
" env:",
|
|
688
|
+
` EXECUTION_FILE: \${{ steps.${CLAUDE_STEP_ID}.outputs.execution_file }}`,
|
|
689
|
+
" run: |",
|
|
690
|
+
' [ -f "$EXECUTION_FILE" ] || exit 0',
|
|
691
|
+
` stop_token="archweave-$(od -An -N16 -tx1 /dev/urandom | tr -d ' \\n')"`,
|
|
692
|
+
' echo "::stop-commands::$stop_token"',
|
|
693
|
+
` jq -c '.[] | select(.type == "result") | .permission_denials[]? | {tool: .tool_name, command: ((.tool_input.command // "") | tostring | .[0:200])}' "$EXECUTION_FILE" || echo "Could not read the execution file."`,
|
|
694
|
+
' echo "::$stop_token::"'
|
|
516
695
|
].join("\n");
|
|
517
696
|
}
|
|
697
|
+
function buildWorkflowYaml(input) {
|
|
698
|
+
return renderReviewWorkflow(input, {
|
|
699
|
+
name: "Claude Code Review",
|
|
700
|
+
secretName: SECRET_NAME,
|
|
701
|
+
// The Claude GitHub App signs in with the workflow's OIDC token (claude-code-action's FAQ).
|
|
702
|
+
idToken: true,
|
|
703
|
+
reviewSteps: [
|
|
704
|
+
renderCheckoutStep(REVIEW_GATE_CONDITION),
|
|
705
|
+
[
|
|
706
|
+
"- name: Run Claude review",
|
|
707
|
+
` id: ${CLAUDE_STEP_ID}`,
|
|
708
|
+
` if: ${REVIEW_GATE_CONDITION}`,
|
|
709
|
+
` uses: ${pinnedUses("anthropics/claude-code-action")}`,
|
|
710
|
+
" with:",
|
|
711
|
+
` claude_code_oauth_token: \${{ secrets.${SECRET_NAME} }}`,
|
|
712
|
+
" prompt: |",
|
|
713
|
+
indentBlock(composePrompt(input), 6),
|
|
714
|
+
" claude_args: |",
|
|
715
|
+
` --allowedTools "${ALLOWED_TOOLS}"`
|
|
716
|
+
].join("\n"),
|
|
717
|
+
renderDeniedToolsStep()
|
|
718
|
+
]
|
|
719
|
+
});
|
|
720
|
+
}
|
|
518
721
|
var claudeProvider = {
|
|
519
722
|
id: "claude",
|
|
520
723
|
workflowFileName: ".github/workflows/claude-code-review.yml",
|
|
@@ -553,49 +756,41 @@ function composePrompt2(input) {
|
|
|
553
756
|
return sections.join("\n\n---\n\n");
|
|
554
757
|
}
|
|
555
758
|
function buildWorkflowYaml2(input) {
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
[
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
"
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
"
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
"",
|
|
592
|
-
"jobs:",
|
|
593
|
-
" review:",
|
|
594
|
-
" runs-on: ubuntu-latest",
|
|
595
|
-
" steps:",
|
|
596
|
-
indentBlock(steps.join("\n\n"), 6),
|
|
597
|
-
""
|
|
598
|
-
].join("\n");
|
|
759
|
+
return renderReviewWorkflow(input, {
|
|
760
|
+
name: "Cursor Code Review",
|
|
761
|
+
secretName: SECRET_NAME2,
|
|
762
|
+
// An API key: the Cursor CLI needs no OIDC token.
|
|
763
|
+
idToken: false,
|
|
764
|
+
reviewSteps: [
|
|
765
|
+
renderCheckoutStep(REVIEW_GATE_CONDITION),
|
|
766
|
+
[
|
|
767
|
+
"- name: Install cursor-agent CLI",
|
|
768
|
+
` if: ${REVIEW_GATE_CONDITION}`,
|
|
769
|
+
" run: |",
|
|
770
|
+
// HTTPS only, TLS 1.2 or newer. Cursor publishes no pinned version or checksum for
|
|
771
|
+
// this installer; docs/pr-review.md states what that leaves open.
|
|
772
|
+
" curl --proto '=https' --tlsv1.2 -fsS https://cursor.com/install | bash",
|
|
773
|
+
// The docs name ~/.cursor/bin; the installer currently links `agent` into ~/.local/bin.
|
|
774
|
+
// Both go on the PATH, so the next step finds it wherever the installer puts it.
|
|
775
|
+
' echo "$HOME/.local/bin" >> "$GITHUB_PATH"',
|
|
776
|
+
' echo "$HOME/.cursor/bin" >> "$GITHUB_PATH"'
|
|
777
|
+
].join("\n"),
|
|
778
|
+
[
|
|
779
|
+
"- name: Run Cursor review",
|
|
780
|
+
` if: ${REVIEW_GATE_CONDITION}`,
|
|
781
|
+
" env:",
|
|
782
|
+
` CURSOR_API_KEY: \${{ secrets.${SECRET_NAME2} }}`,
|
|
783
|
+
" GH_TOKEN: ${{ github.token }}",
|
|
784
|
+
" run: |",
|
|
785
|
+
// cursor-agent stops at "Workspace Trust Required" without --trust (2026.10.22 on
|
|
786
|
+
// Actions); the folder is this run's own checkout, and forks never get the secret.
|
|
787
|
+
` agent --trust -p "$(cat <<'${PROMPT_HEREDOC_DELIMITER}'`,
|
|
788
|
+
indentBlock(composePrompt2(input), 4),
|
|
789
|
+
` ${PROMPT_HEREDOC_DELIMITER}`,
|
|
790
|
+
' )"'
|
|
791
|
+
].join("\n")
|
|
792
|
+
]
|
|
793
|
+
});
|
|
599
794
|
}
|
|
600
795
|
var cursorProvider = {
|
|
601
796
|
id: "cursor",
|
|
@@ -633,41 +828,33 @@ function composePrompt3(input) {
|
|
|
633
828
|
sections.push(renderFindingsBlockInstruction(input.locale));
|
|
634
829
|
return sections.join("\n\n---\n\n");
|
|
635
830
|
}
|
|
831
|
+
var GEMINI_CLI_VERSION = "0.65.0";
|
|
636
832
|
function buildWorkflowYaml3(input) {
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
"
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
renderPermissionsBlock(),
|
|
663
|
-
"",
|
|
664
|
-
"jobs:",
|
|
665
|
-
" review:",
|
|
666
|
-
" runs-on: ubuntu-latest",
|
|
667
|
-
" steps:",
|
|
668
|
-
indentBlock(steps.join("\n\n"), 6),
|
|
669
|
-
""
|
|
670
|
-
].join("\n");
|
|
833
|
+
return renderReviewWorkflow(input, {
|
|
834
|
+
name: "Gemini Code Review",
|
|
835
|
+
secretName: SECRET_NAME3,
|
|
836
|
+
// An API key: run-gemini-cli uses OIDC only for Google Cloud workload identity federation.
|
|
837
|
+
idToken: false,
|
|
838
|
+
reviewSteps: [
|
|
839
|
+
renderCheckoutStep(REVIEW_GATE_CONDITION),
|
|
840
|
+
[
|
|
841
|
+
"- name: Run Gemini review",
|
|
842
|
+
` if: ${REVIEW_GATE_CONDITION}`,
|
|
843
|
+
` uses: ${pinnedUses("google-github-actions/run-gemini-cli")}`,
|
|
844
|
+
" env:",
|
|
845
|
+
" GITHUB_TOKEN: ${{ github.token }}",
|
|
846
|
+
// Gemini CLI 0.65 refuses to run in a folder it hasn't been told to trust (measured
|
|
847
|
+
// 2026-10-25 on Actions). The folder is this run's own checkout; pull requests from forks
|
|
848
|
+
// never get here, because they get no secret.
|
|
849
|
+
" GEMINI_CLI_TRUST_WORKSPACE: 'true'",
|
|
850
|
+
" with:",
|
|
851
|
+
` gemini_api_key: \${{ secrets.${SECRET_NAME3} }}`,
|
|
852
|
+
` gemini_cli_version: '${GEMINI_CLI_VERSION}'`,
|
|
853
|
+
" prompt: |",
|
|
854
|
+
indentBlock(composePrompt3(input), 6)
|
|
855
|
+
].join("\n")
|
|
856
|
+
]
|
|
857
|
+
});
|
|
671
858
|
}
|
|
672
859
|
var geminiProvider = {
|
|
673
860
|
id: "gemini",
|
|
@@ -706,64 +893,49 @@ function composePrompt4(input) {
|
|
|
706
893
|
return sections.join("\n\n---\n\n");
|
|
707
894
|
}
|
|
708
895
|
function buildWorkflowYaml4(input) {
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
[
|
|
715
|
-
"
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
"
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
"
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
"name: GPT Code Review",
|
|
753
|
-
"",
|
|
754
|
-
renderTriggerBlock(),
|
|
755
|
-
"",
|
|
756
|
-
renderConcurrencyBlock(),
|
|
757
|
-
"",
|
|
758
|
-
renderPermissionsBlock(),
|
|
759
|
-
"",
|
|
760
|
-
"jobs:",
|
|
761
|
-
" review:",
|
|
762
|
-
" runs-on: ubuntu-latest",
|
|
763
|
-
" steps:",
|
|
764
|
-
indentBlock(steps.join("\n\n"), 6),
|
|
765
|
-
""
|
|
766
|
-
].join("\n");
|
|
896
|
+
return renderReviewWorkflow(input, {
|
|
897
|
+
name: "GPT Code Review",
|
|
898
|
+
secretName: SECRET_NAME4,
|
|
899
|
+
// An API key: codex-action needs no OIDC token.
|
|
900
|
+
idToken: false,
|
|
901
|
+
reviewSteps: [
|
|
902
|
+
renderCheckoutStep(REVIEW_GATE_CONDITION, ["ref: refs/pull/${{ github.event.pull_request.number }}/merge"]),
|
|
903
|
+
[
|
|
904
|
+
"- name: Fetch base and head refs for the PR",
|
|
905
|
+
` if: ${REVIEW_GATE_CONDITION}`,
|
|
906
|
+
" env:",
|
|
907
|
+
" PR_BASE_REF: ${{ github.event.pull_request.base.ref }}",
|
|
908
|
+
" PR_NUMBER: ${{ github.event.pull_request.number }}",
|
|
909
|
+
" run: |",
|
|
910
|
+
" # Explicit destinations: the sandboxed review step has no network, so both sides",
|
|
911
|
+
" # of the diff must survive as stable local refs (FETCH_HEAD alone is ambiguous).",
|
|
912
|
+
" git fetch --no-tags origin \\",
|
|
913
|
+
' "+refs/heads/$PR_BASE_REF:refs/remotes/origin/pr-base" \\',
|
|
914
|
+
' "+refs/pull/$PR_NUMBER/head:refs/remotes/origin/pr-head"'
|
|
915
|
+
].join("\n"),
|
|
916
|
+
[
|
|
917
|
+
"- name: Run Codex review",
|
|
918
|
+
" id: codex-review",
|
|
919
|
+
` if: ${REVIEW_GATE_CONDITION}`,
|
|
920
|
+
` uses: ${pinnedUses("openai/codex-action")}`,
|
|
921
|
+
" with:",
|
|
922
|
+
` openai-api-key: \${{ secrets.${SECRET_NAME4} }}`,
|
|
923
|
+
' permission-profile: ":read-only"',
|
|
924
|
+
" prompt: |",
|
|
925
|
+
indentBlock(composePrompt4(input), 6)
|
|
926
|
+
].join("\n"),
|
|
927
|
+
[
|
|
928
|
+
"- name: Post review comment",
|
|
929
|
+
` if: ${REVIEW_GATE_CONDITION}`,
|
|
930
|
+
" env:",
|
|
931
|
+
" GH_TOKEN: ${{ github.token }}",
|
|
932
|
+
" PR_NUMBER: ${{ github.event.pull_request.number }}",
|
|
933
|
+
" REVIEW_BODY: ${{ steps.codex-review.outputs.final-message }}",
|
|
934
|
+
" run: |",
|
|
935
|
+
' printf %s "$REVIEW_BODY" | gh pr comment "$PR_NUMBER" --body-file -'
|
|
936
|
+
].join("\n")
|
|
937
|
+
]
|
|
938
|
+
});
|
|
767
939
|
}
|
|
768
940
|
var gptProvider = {
|
|
769
941
|
id: "gpt",
|
|
@@ -850,7 +1022,8 @@ var REVIEW_CHECK_IDS = [
|
|
|
850
1022
|
"review-alt-file-coexistence",
|
|
851
1023
|
"review-config-consistency",
|
|
852
1024
|
"review-workflow-providers",
|
|
853
|
-
"review-workflow-pnpm"
|
|
1025
|
+
"review-workflow-pnpm",
|
|
1026
|
+
"review-workflow-fail-open"
|
|
854
1027
|
];
|
|
855
1028
|
var WORKFLOWS_DIR = ".github/workflows";
|
|
856
1029
|
function archweaveWorkflows(root) {
|
|
@@ -907,6 +1080,25 @@ function buildReviewPnpmCheck(root, workflows) {
|
|
|
907
1080
|
suggestedAction: "Regenerate the workflow: nx g @clearlydev/nx-archweave:add-review --onConflict=overwrite."
|
|
908
1081
|
});
|
|
909
1082
|
}
|
|
1083
|
+
var FAIL_CLOSED_VERDICT_MODE = "ARCHWEAVE_VERDICT_OUTPUT: 'github-output'";
|
|
1084
|
+
function buildReviewFailOpenCheck(root, workflows) {
|
|
1085
|
+
const id = "review-workflow-fail-open";
|
|
1086
|
+
const label = "Review check fails closed";
|
|
1087
|
+
if (workflows.length === 0)
|
|
1088
|
+
return createDoctorCheck({ id, label, status: "not-applicable", message: "no nx-archweave review workflow", evidence: [] });
|
|
1089
|
+
const failOpen = workflows.filter((path) => !readFileSync2(join2(root, path), "utf-8").includes(FAIL_CLOSED_VERDICT_MODE));
|
|
1090
|
+
if (failOpen.length === 0) {
|
|
1091
|
+
return createDoctorCheck({ id, label, status: "pass", message: "a review run that leaves no readable summary fails the review check", evidence: workflows });
|
|
1092
|
+
}
|
|
1093
|
+
return createDoctorCheck({
|
|
1094
|
+
id,
|
|
1095
|
+
label,
|
|
1096
|
+
status: "error",
|
|
1097
|
+
message: "fail-open workflow: generated before nx-archweave 0.2.2, it passes the review check when the AI review posts no summary or one that cannot be read, so an unreviewed pull request can merge",
|
|
1098
|
+
evidence: failOpen,
|
|
1099
|
+
suggestedAction: "Re-run add-review: nx g @clearlydev/nx-archweave:add-review --onConflict=overwrite. The report shows the difference before anything is replaced."
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
910
1102
|
function buildReviewChecks(root) {
|
|
911
1103
|
const config = readToolkitConfigState(root);
|
|
912
1104
|
const mainWorkflowPath = REVIEW_PROVIDERS[resolveProviderId(config)].workflowFileName;
|
|
@@ -920,7 +1112,8 @@ function buildReviewChecks(root) {
|
|
|
920
1112
|
buildReviewAltFileCoexistenceCheck(mainWorkflowPath, altWorkflowPath, mainExists, altExists),
|
|
921
1113
|
buildReviewConfigConsistencyCheck(root, mainWorkflowPath, mainExists, config),
|
|
922
1114
|
buildReviewProvidersCheck(workflows),
|
|
923
|
-
buildReviewPnpmCheck(root, workflows)
|
|
1115
|
+
buildReviewPnpmCheck(root, workflows),
|
|
1116
|
+
buildReviewFailOpenCheck(root, workflows)
|
|
924
1117
|
];
|
|
925
1118
|
}
|
|
926
1119
|
function buildReviewWorkflowPresentCheck(mainWorkflowPath, mainExists, mainIsOurs, config) {
|
|
@@ -1038,13 +1231,14 @@ function buildReviewConfigConsistencyCheck(root, mainWorkflowPath, mainExists, c
|
|
|
1038
1231
|
evidence: [mainWorkflowPath, CONFIG_PATH]
|
|
1039
1232
|
});
|
|
1040
1233
|
}
|
|
1234
|
+
const earlierVersion = !/^ {2}review-comment:$/m.test(workflowContent);
|
|
1041
1235
|
return createDoctorCheck({
|
|
1042
1236
|
id: "review-config-consistency",
|
|
1043
1237
|
label: "Review config/workflow consistency",
|
|
1044
1238
|
status: "warning",
|
|
1045
|
-
message: `${mainWorkflowPath} differs from what ${CONFIG_PATH}'s review options would generate`,
|
|
1239
|
+
message: earlierVersion ? `${mainWorkflowPath} was generated by an earlier nx-archweave version: it installs dependencies in the job that holds the provider secret` : `${mainWorkflowPath} differs from what ${CONFIG_PATH}'s review options would generate`,
|
|
1046
1240
|
evidence: [mainWorkflowPath, CONFIG_PATH],
|
|
1047
|
-
suggestedAction: `Re-run the add-review generator with --onConflict=overwrite to re-sync ${mainWorkflowPath} with ${CONFIG_PATH}, or update ${CONFIG_PATH} to match the installed file.`
|
|
1241
|
+
suggestedAction: earlierVersion ? `Re-run add-review --onConflict=overwrite to regenerate ${mainWorkflowPath}: the verdict then runs in a job without the secret, an OIDC token or install scripts. Edits you made to the file are replaced; the report shows the difference first.` : `Re-run the add-review generator with --onConflict=overwrite to re-sync ${mainWorkflowPath} with ${CONFIG_PATH}, or update ${CONFIG_PATH} to match the installed file.`
|
|
1048
1242
|
});
|
|
1049
1243
|
}
|
|
1050
1244
|
|
|
@@ -1158,15 +1352,22 @@ var BACKEND_CHECK_IDS = [
|
|
|
1158
1352
|
"backend-config-consistency",
|
|
1159
1353
|
"shared-types-lib",
|
|
1160
1354
|
"boundary-constraints-applied",
|
|
1161
|
-
"boundary-tags-exist"
|
|
1355
|
+
"boundary-tags-exist",
|
|
1356
|
+
"boundary-untagged-projects"
|
|
1162
1357
|
];
|
|
1163
1358
|
var CHECK_LABELS = {
|
|
1164
1359
|
"backend-framework": "Backend framework (root package.json only)",
|
|
1165
1360
|
"backend-config-consistency": "Backend config consistency",
|
|
1166
1361
|
"shared-types-lib": "shared-types library",
|
|
1167
1362
|
"boundary-constraints-applied": "Module-boundary constraints",
|
|
1168
|
-
"boundary-tags-exist": "Boundary tags exist"
|
|
1363
|
+
"boundary-tags-exist": "Boundary tags exist",
|
|
1364
|
+
"boundary-untagged-projects": "Projects without a boundary tag"
|
|
1169
1365
|
};
|
|
1366
|
+
var UNTAGGED_LISTED = 10;
|
|
1367
|
+
function isEndToEndRoot(projectRoot) {
|
|
1368
|
+
const base = projectRoot.split("/").pop() ?? "";
|
|
1369
|
+
return base === "e2e" || base.endsWith("-e2e");
|
|
1370
|
+
}
|
|
1170
1371
|
function collectScopeTags(root, projects) {
|
|
1171
1372
|
if (projects !== void 0)
|
|
1172
1373
|
return new Set(projects.flatMap((project) => project.tags));
|
|
@@ -1323,13 +1524,15 @@ function buildBackendChecks(root, projects) {
|
|
|
1323
1524
|
suggestedAction: classification === "enforced" ? void 0 : "Run add-backend with --applyEslint, or merge its snippet yourself."
|
|
1324
1525
|
}));
|
|
1325
1526
|
if (classification !== "enforced") {
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1527
|
+
for (const id of ["boundary-tags-exist", "boundary-untagged-projects"]) {
|
|
1528
|
+
checks.push(createDoctorCheck({
|
|
1529
|
+
id,
|
|
1530
|
+
label: CHECK_LABELS[id],
|
|
1531
|
+
status: "not-applicable",
|
|
1532
|
+
message: "Skipped \u2014 boundary constraints are not enforced (see boundary-constraints-applied)",
|
|
1533
|
+
evidence: []
|
|
1534
|
+
}));
|
|
1535
|
+
}
|
|
1333
1536
|
} else {
|
|
1334
1537
|
const tags = collectScopeTags(root, projects);
|
|
1335
1538
|
const required = readsBackendOnly(root) ? [boundaryTags.backend] : [boundaryTags.frontend, boundaryTags.backend];
|
|
@@ -1342,9 +1545,36 @@ function buildBackendChecks(root, projects) {
|
|
|
1342
1545
|
evidence: missing.length === 0 ? [] : missing,
|
|
1343
1546
|
suggestedAction: missing.length === 0 ? void 0 : "Add the missing tag(s) to the app project.json tags or package.json nx.tags (add-backend does this for the two apps)."
|
|
1344
1547
|
}));
|
|
1548
|
+
checks.push(buildUntaggedProjectsCheck(projects, [boundaryTags.frontend, boundaryTags.backend, boundaryTags.shared]));
|
|
1345
1549
|
}
|
|
1346
1550
|
return checks;
|
|
1347
1551
|
}
|
|
1552
|
+
function buildUntaggedProjectsCheck(projects, roleTags) {
|
|
1553
|
+
const id = "boundary-untagged-projects";
|
|
1554
|
+
if (projects === void 0) {
|
|
1555
|
+
return createDoctorCheck({
|
|
1556
|
+
id,
|
|
1557
|
+
label: CHECK_LABELS[id],
|
|
1558
|
+
status: "not-applicable",
|
|
1559
|
+
message: "Skipped \u2014 needs the workspace's Nx project list (run doctor through nx g)",
|
|
1560
|
+
evidence: []
|
|
1561
|
+
});
|
|
1562
|
+
}
|
|
1563
|
+
const untagged = projects.filter((project) => project.root !== "" && project.root !== "." && !isEndToEndRoot(project.root)).filter((project) => !project.tags.some((tag) => roleTags.includes(tag))).map((project) => project.name);
|
|
1564
|
+
if (untagged.length === 0) {
|
|
1565
|
+
return createDoctorCheck({ id, label: CHECK_LABELS[id], status: "pass", message: `every project carries one of ${roleTags.join(", ")}`, evidence: [] });
|
|
1566
|
+
}
|
|
1567
|
+
const listed = untagged.slice(0, UNTAGGED_LISTED).join(", ");
|
|
1568
|
+
const more = untagged.length > UNTAGGED_LISTED ? ` and ${untagged.length - UNTAGGED_LISTED} more` : "";
|
|
1569
|
+
return createDoctorCheck({
|
|
1570
|
+
id,
|
|
1571
|
+
label: CHECK_LABELS[id],
|
|
1572
|
+
status: "warning",
|
|
1573
|
+
message: `${untagged.length} project(s) carry none of ${roleTags.join(", ")}, so the frontend/backend deny rules don't cover them: ${listed}${more}`,
|
|
1574
|
+
evidence: untagged,
|
|
1575
|
+
suggestedAction: `Give each one the tag of its side (${roleTags.join(", ")}) in package.json nx.tags or project.json tags. When you generate a library, pass it right away: nx g @nx/js:lib libs/<name> --tags=${roleTags[2]}.`
|
|
1576
|
+
});
|
|
1577
|
+
}
|
|
1348
1578
|
|
|
1349
1579
|
// ../template-engine/dist/doctor/fsd-like-check.js
|
|
1350
1580
|
import { existsSync as existsSync4, readdirSync as readdirSync3 } from "node:fs";
|
|
@@ -1595,7 +1825,7 @@ var NOT_APPLICABLE_IDS = [
|
|
|
1595
1825
|
...REVIEW_CHECK_IDS,
|
|
1596
1826
|
...BACKEND_CHECK_IDS
|
|
1597
1827
|
];
|
|
1598
|
-
function runDoctorChecks(root, projects) {
|
|
1828
|
+
function runDoctorChecks(root, projects, nxSupport) {
|
|
1599
1829
|
const env = detectEnvironment(root);
|
|
1600
1830
|
if (!env.hasNxWorkspace) {
|
|
1601
1831
|
const checks2 = [
|
|
@@ -1625,7 +1855,7 @@ function runDoctorChecks(root, projects) {
|
|
|
1625
1855
|
message: "nx.json found",
|
|
1626
1856
|
evidence: ["nx.json"]
|
|
1627
1857
|
}),
|
|
1628
|
-
buildNxVersionCheck(env),
|
|
1858
|
+
buildNxVersionCheck(env, nxSupport),
|
|
1629
1859
|
buildFsdLikeCheck(root, projects),
|
|
1630
1860
|
buildFsdEnforcementCheck(root, projects),
|
|
1631
1861
|
buildFsdRuleAnchoredCheck(root, projects),
|
|
@@ -1654,12 +1884,25 @@ function runDoctorChecks(root, projects) {
|
|
|
1654
1884
|
const overallStatus = computeOverallStatus(checks);
|
|
1655
1885
|
return { checks, overallStatus };
|
|
1656
1886
|
}
|
|
1657
|
-
var
|
|
1887
|
+
var VALIDATED_NX_MAJORS = [23, 24];
|
|
1888
|
+
var VALIDATED_RANGE_TEXT = `${VALIDATED_NX_MAJORS[0]}.x\u2013${VALIDATED_NX_MAJORS[VALIDATED_NX_MAJORS.length - 1]}.x`;
|
|
1658
1889
|
function extractMajor(version) {
|
|
1659
1890
|
const match = /(\d+)/.exec(version);
|
|
1660
1891
|
return match ? Number(match[1]) : null;
|
|
1661
1892
|
}
|
|
1662
|
-
function buildNxVersionCheck(env) {
|
|
1893
|
+
function buildNxVersionCheck(env, nxSupport) {
|
|
1894
|
+
if (nxSupport?.inRange === false && nxSupport.installedVersion !== void 0) {
|
|
1895
|
+
const major2 = extractMajor(nxSupport.installedVersion);
|
|
1896
|
+
const supported = `${nxSupport.supportedMajors.min}.x\u2013${nxSupport.supportedMajors.max}.x`;
|
|
1897
|
+
return createDoctorCheck({
|
|
1898
|
+
id: "nx-version",
|
|
1899
|
+
label: "Nx version",
|
|
1900
|
+
status: "error",
|
|
1901
|
+
message: `Nx ${nxSupport.installedVersion} is installed, outside the majors the plugin's @nx/devkit ${nxSupport.devkitVersion} supports (nx ${supported}) \u2014 the generators may fail to load or misbehave`,
|
|
1902
|
+
evidence: [`nx@${nxSupport.installedVersion}`, `@nx/devkit@${nxSupport.devkitVersion}`],
|
|
1903
|
+
suggestedAction: `Install the @clearlydev/nx-archweave release built for Nx ${major2 ?? "this"}${major2 === null ? "" : ".x"}, or keep nx at ${supported}.`
|
|
1904
|
+
});
|
|
1905
|
+
}
|
|
1663
1906
|
if (env.rootPackageJsonError) {
|
|
1664
1907
|
return createDoctorCheck({
|
|
1665
1908
|
id: "nx-version",
|
|
@@ -1689,30 +1932,30 @@ function buildNxVersionCheck(env) {
|
|
|
1689
1932
|
evidence: [`nx@${env.nxVersion}`]
|
|
1690
1933
|
});
|
|
1691
1934
|
}
|
|
1692
|
-
if (major
|
|
1935
|
+
if (VALIDATED_NX_MAJORS.includes(major)) {
|
|
1693
1936
|
return createDoctorCheck({
|
|
1694
1937
|
id: "nx-version",
|
|
1695
1938
|
label: "Nx version",
|
|
1696
1939
|
status: "pass",
|
|
1697
|
-
message: `Nx ${env.nxVersion} \u2014 within the toolkit-validated range (${
|
|
1940
|
+
message: `Nx ${env.nxVersion} \u2014 within the toolkit-validated range (${VALIDATED_RANGE_TEXT})`,
|
|
1698
1941
|
evidence: [`nx@${env.nxVersion}`]
|
|
1699
1942
|
});
|
|
1700
1943
|
}
|
|
1701
|
-
if (major <
|
|
1944
|
+
if (major < VALIDATED_NX_MAJORS[0]) {
|
|
1702
1945
|
return createDoctorCheck({
|
|
1703
1946
|
id: "nx-version",
|
|
1704
1947
|
label: "Nx version",
|
|
1705
1948
|
status: "warning",
|
|
1706
|
-
message: `Nx ${env.nxVersion} is below the toolkit-validated range (${
|
|
1949
|
+
message: `Nx ${env.nxVersion} is below the toolkit-validated range (${VALIDATED_RANGE_TEXT}) \u2014 generator behavior is not guaranteed`,
|
|
1707
1950
|
evidence: [`nx@${env.nxVersion}`],
|
|
1708
|
-
suggestedAction: `Upgrade the workspace to Nx ${
|
|
1951
|
+
suggestedAction: `Upgrade the workspace to Nx ${VALIDATED_NX_MAJORS[VALIDATED_NX_MAJORS.length - 1]}.x before running the generators.`
|
|
1709
1952
|
});
|
|
1710
1953
|
}
|
|
1711
1954
|
return createDoctorCheck({
|
|
1712
1955
|
id: "nx-version",
|
|
1713
1956
|
label: "Nx version",
|
|
1714
1957
|
status: "warning",
|
|
1715
|
-
message: `Nx ${env.nxVersion} is above the toolkit-validated range (${
|
|
1958
|
+
message: `Nx ${env.nxVersion} is above the toolkit-validated range (${VALIDATED_RANGE_TEXT}) \u2014 may work, unvalidated`,
|
|
1716
1959
|
evidence: [`nx@${env.nxVersion}`],
|
|
1717
1960
|
suggestedAction: "Proceed with care and report issues; validation for newer majors lands with toolkit updates."
|
|
1718
1961
|
});
|
|
@@ -2644,7 +2887,7 @@ function diskRunNx(root) {
|
|
|
2644
2887
|
var DOCTOR_TARGET_ENVIRONMENT = "claude-code";
|
|
2645
2888
|
async function buildDoctorReportWithIntegrations(root, options = {}, deps = {}) {
|
|
2646
2889
|
const integrationsRoot = deps.integrationsRoot ?? createIntegrationsCompositionRoot();
|
|
2647
|
-
const baseReport = runDoctorChecks(root, options.projects);
|
|
2890
|
+
const baseReport = runDoctorChecks(root, options.projects, options.nxSupport);
|
|
2648
2891
|
const declaredIds = readEnabledIntegrationIds(root);
|
|
2649
2892
|
const runNx = diskRunNx(root);
|
|
2650
2893
|
const driftResults = await detectIntegrationsDrift({
|
|
@@ -3093,11 +3336,49 @@ function buildPolicySuggestion(tree, sourceRoot, current) {
|
|
|
3093
3336
|
});
|
|
3094
3337
|
}
|
|
3095
3338
|
|
|
3339
|
+
// src/generators/doctor/nx-support.ts
|
|
3340
|
+
import { readFileSync as readFileSync9 } from "node:fs";
|
|
3341
|
+
import { createRequire } from "node:module";
|
|
3342
|
+
import { join as join12 } from "node:path";
|
|
3343
|
+
var MAJORS_EITHER_SIDE = 1;
|
|
3344
|
+
function majorOf(version) {
|
|
3345
|
+
const match = /^(\d+)\./.exec(version.trim());
|
|
3346
|
+
return match ? Number(match[1]) : void 0;
|
|
3347
|
+
}
|
|
3348
|
+
function readOwnDevkitVersion() {
|
|
3349
|
+
try {
|
|
3350
|
+
const path = createRequire(import.meta.url).resolve("@nx/devkit/package.json");
|
|
3351
|
+
const version = JSON.parse(readFileSync9(path, "utf-8")).version;
|
|
3352
|
+
return typeof version === "string" ? version : void 0;
|
|
3353
|
+
} catch {
|
|
3354
|
+
return void 0;
|
|
3355
|
+
}
|
|
3356
|
+
}
|
|
3357
|
+
function readInstalledNx(root) {
|
|
3358
|
+
try {
|
|
3359
|
+
const version = JSON.parse(readFileSync9(join12(root, "node_modules", "nx", "package.json"), "utf-8")).version;
|
|
3360
|
+
return typeof version === "string" ? version : void 0;
|
|
3361
|
+
} catch {
|
|
3362
|
+
return void 0;
|
|
3363
|
+
}
|
|
3364
|
+
}
|
|
3365
|
+
function readNxSupport(root, deps = {}) {
|
|
3366
|
+
const devkitVersion = (deps.devkitVersion ?? readOwnDevkitVersion)();
|
|
3367
|
+
const devkitMajor = devkitVersion === void 0 ? void 0 : majorOf(devkitVersion);
|
|
3368
|
+
if (devkitVersion === void 0 || devkitMajor === void 0) return void 0;
|
|
3369
|
+
const supportedMajors = { min: devkitMajor - MAJORS_EITHER_SIDE, max: devkitMajor + MAJORS_EITHER_SIDE };
|
|
3370
|
+
const installedVersion = (deps.installedNx ?? readInstalledNx)(root);
|
|
3371
|
+
const installedMajor = installedVersion === void 0 ? void 0 : majorOf(installedVersion);
|
|
3372
|
+
const inRange = installedMajor === void 0 ? void 0 : installedMajor >= supportedMajors.min && installedMajor <= supportedMajors.max;
|
|
3373
|
+
return { devkitVersion, supportedMajors, installedVersion, inRange };
|
|
3374
|
+
}
|
|
3375
|
+
|
|
3096
3376
|
// src/generators/doctor/generator.ts
|
|
3097
3377
|
async function doctorGenerator(tree, options) {
|
|
3098
3378
|
const base = await buildDoctorReportWithIntegrations(tree.root, {
|
|
3099
3379
|
liveIntegrations: options.liveIntegrations ?? false,
|
|
3100
|
-
projects: doctorProjects(tree)
|
|
3380
|
+
projects: doctorProjects(tree),
|
|
3381
|
+
nxSupport: readNxSupport(tree.root)
|
|
3101
3382
|
});
|
|
3102
3383
|
const checks = base.overallStatus === "unsupported" ? base.checks : [...base.checks, ...buildWorkspaceChecks(tree)];
|
|
3103
3384
|
const report = { checks, overallStatus: base.overallStatus === "unsupported" ? base.overallStatus : computeOverallStatus(checks) };
|