@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
package/README.md
CHANGED
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
Nx generators that get a workspace ready to work on with one preset:
|
|
4
4
|
|
|
5
|
-
- FSD Lite
|
|
5
|
+
- an FSD Lite lint rule for the frontend app's layers (`warn` or `error` by preset), plus slice generators
|
|
6
|
+
- frontend/backend boundaries as lint errors between the projects it tags, when there is a backend
|
|
6
7
|
- AI agent rules
|
|
7
8
|
- GitHub templates
|
|
8
|
-
- optional AI PR review (Claude
|
|
9
|
+
- optional AI PR review with the provider you choose (Claude supported; Gemini, GPT and Cursor in beta)
|
|
9
10
|
|
|
10
11
|
Starting a brand-new workspace instead? Use [`create-nx-archweave`](https://www.npmjs.com/package/create-nx-archweave). It creates the workspace, installs this plugin, and runs `init`, all in one command.
|
|
11
12
|
|
|
@@ -32,7 +33,7 @@ bun nx g @clearlydev/nx-archweave:doctor # bun
|
|
|
32
33
|
- **Requirements:** Node >= 22.18, plus one of npm, pnpm >= 10, yarn (classic or berry) or bun. All four package managers are first-class.
|
|
33
34
|
- Generators detect the workspace's real package manager from its lockfiles.
|
|
34
35
|
- The examples below use `pnpm nx g`. With another package manager, run the same generator the way the `doctor` commands above show.
|
|
35
|
-
- The package ships as a
|
|
36
|
+
- The package ships its own code as a bundle. Its runtime dependencies are `@nx/devkit` and `jsonc-parser`, which nx already depends on at the same version.
|
|
36
37
|
|
|
37
38
|
## Quick start
|
|
38
39
|
|
|
@@ -213,9 +214,11 @@ Review is fully optional and isn't tied to one provider.
|
|
|
213
214
|
| `--provider` | Workflow file | Required secret |
|
|
214
215
|
| --- | --- | --- |
|
|
215
216
|
| `claude` (default) | `.github/workflows/claude-code-review.yml` | `CLAUDE_CODE_OAUTH_TOKEN` |
|
|
216
|
-
| `gemini` | `.github/workflows/gemini-code-review.yml` | `GEMINI_API_KEY` |
|
|
217
|
-
| `gpt` | `.github/workflows/gpt-code-review.yml` | `OPENAI_API_KEY` |
|
|
218
|
-
| `cursor` | `.github/workflows/cursor-code-review.yml` | `CURSOR_API_KEY` |
|
|
217
|
+
| `gemini` (beta) | `.github/workflows/gemini-code-review.yml` | `GEMINI_API_KEY` |
|
|
218
|
+
| `gpt` (beta) | `.github/workflows/gpt-code-review.yml` | `OPENAI_API_KEY` |
|
|
219
|
+
| `cursor` (beta) | `.github/workflows/cursor-code-review.yml` | `CURSOR_API_KEY` |
|
|
220
|
+
|
|
221
|
+
What each provider has been checked on, where to get its secret, and known issues: [Provider status](https://github.com/clearlydev/nx-archweave/blob/main/docs/pr-review.md#provider-status).
|
|
219
222
|
|
|
220
223
|
Modes:
|
|
221
224
|
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
// src/bin/review-verdict.ts
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
5
|
-
import {
|
|
5
|
+
import { randomBytes } from "node:crypto";
|
|
6
|
+
import { appendFileSync, realpathSync } from "node:fs";
|
|
6
7
|
|
|
7
8
|
// ../review-policy/dist/schema/review-finding.js
|
|
8
9
|
function computeVerdict(findings) {
|
|
@@ -149,15 +150,29 @@ var NO_ROUND_NOTICE = {
|
|
|
149
150
|
en: "\u26A0\uFE0F This run of the review posted no summary comment for the current commit, so there is no verdict for it (an earlier round is never reused). The verdict gate stays green; please review this PR manually.",
|
|
150
151
|
ko: "\u26A0\uFE0F \uC774\uBC88 \uB9AC\uBDF0 \uC2E4\uD589\uC774 \uD604\uC7AC \uCEE4\uBC0B\uC5D0 \uB300\uD55C \uC694\uC57D \uCF54\uBA58\uD2B8\uB97C \uAC8C\uC2DC\uD558\uC9C0 \uC54A\uC544 verdict\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4(\uC774\uC804 \uB77C\uC6B4\uB4DC\uB294 \uC7AC\uC0AC\uC6A9\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4). verdict \uAC8C\uC774\uD2B8\uB294 \uB179\uC0C9\uC73C\uB85C \uB450\uB2C8 \uC774 PR\uC740 \uC0AC\uB78C\uC774 \uC9C1\uC811 \uB9AC\uBDF0\uD574 \uC8FC\uC138\uC694."
|
|
151
152
|
};
|
|
153
|
+
var NO_SUMMARY_NOTICE = {
|
|
154
|
+
en: "\u26D4 AI review posted no summary for this commit, so there is no verdict for it and the review check fails. Review this PR manually, or re-run all jobs to review it again.",
|
|
155
|
+
ko: "\u26D4 AI \uB9AC\uBDF0\uAC00 \uC774 \uCEE4\uBC0B\uC5D0 \uB300\uD55C \uC694\uC57D\uC744 \uAC8C\uC2DC\uD558\uC9C0 \uC54A\uC544 verdict\uAC00 \uC5C6\uACE0, review \uCCB4\uD06C\uB294 \uC2E4\uD328\uD569\uB2C8\uB2E4. \uC774 PR\uC744 \uC9C1\uC811 \uB9AC\uBDF0\uD558\uAC70\uB098, \uBAA8\uB4E0 \uC7A1\uC744 \uB2E4\uC2DC \uC2E4\uD589\uD574 \uB2E4\uC2DC \uB9AC\uBDF0\uBC1B\uC73C\uC138\uC694."
|
|
156
|
+
};
|
|
157
|
+
var UNREADABLE_SUMMARY_NOTICE = {
|
|
158
|
+
en: "\u26D4 AI review posted a summary whose findings block could not be machine-read (missing or invalid), so there is no verdict for this commit and the review check fails. Review this PR manually, or re-run all jobs to review it again.",
|
|
159
|
+
ko: "\u26D4 AI \uB9AC\uBDF0 \uC694\uC57D\uC758 findings \uBE14\uB85D\uC744 \uAE30\uACC4 \uD310\uB3C5\uD558\uC9C0 \uBABB\uD574(\uB204\uB77D \uB610\uB294 \uC190\uC0C1) \uC774 \uCEE4\uBC0B\uC758 verdict\uAC00 \uC5C6\uACE0, review \uCCB4\uD06C\uB294 \uC2E4\uD328\uD569\uB2C8\uB2E4. \uC774 PR\uC744 \uC9C1\uC811 \uB9AC\uBDF0\uD558\uAC70\uB098, \uBAA8\uB4E0 \uC7A1\uC744 \uB2E4\uC2DC \uC2E4\uD589\uD574 \uB2E4\uC2DC \uB9AC\uBDF0\uBC1B\uC73C\uC138\uC694."
|
|
160
|
+
};
|
|
161
|
+
function renderCommitLine(locale, shortSha) {
|
|
162
|
+
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>`;
|
|
163
|
+
}
|
|
152
164
|
var HEAD_LINE = {
|
|
153
|
-
en: (sha) =>
|
|
154
|
-
ko: (sha) =>
|
|
165
|
+
en: (sha) => renderCommitLine("en", sha.slice(0, 7)),
|
|
166
|
+
ko: (sha) => renderCommitLine("ko", sha.slice(0, 7))
|
|
155
167
|
};
|
|
156
168
|
function withHead(lines, locale, headSha) {
|
|
157
169
|
return (headSha === void 0 ? lines : [...lines, "", HEAD_LINE[locale](headSha)]).join("\n");
|
|
158
170
|
}
|
|
159
|
-
function renderUnparseableNotice(locale, headSha) {
|
|
160
|
-
return withHead([VERDICT_MARKER, UNPARSEABLE_NOTICE[locale]], locale, headSha);
|
|
171
|
+
function renderUnparseableNotice(locale, headSha, failClosed = false) {
|
|
172
|
+
return withHead([VERDICT_MARKER, (failClosed ? UNREADABLE_SUMMARY_NOTICE : UNPARSEABLE_NOTICE)[locale]], locale, headSha);
|
|
173
|
+
}
|
|
174
|
+
function renderNoSummaryNotice(locale, headSha) {
|
|
175
|
+
return withHead([VERDICT_MARKER, NO_SUMMARY_NOTICE[locale]], locale, headSha);
|
|
161
176
|
}
|
|
162
177
|
function renderNoRoundNotice(locale, headSha) {
|
|
163
178
|
return withHead([VERDICT_MARKER, NO_ROUND_NOTICE[locale]], locale, headSha);
|
|
@@ -205,14 +220,16 @@ function computeRoundHistory(rounds) {
|
|
|
205
220
|
}
|
|
206
221
|
function runVerdictGate(input) {
|
|
207
222
|
const rounds = collectRounds(input.comments);
|
|
208
|
-
|
|
209
|
-
|
|
223
|
+
const reviewRan = input.reviewRan === true;
|
|
224
|
+
if (rounds.length === 0) {
|
|
225
|
+
return reviewRan ? { kind: "no-summary", commentBody: renderNoSummaryNotice(input.locale, input.headSha) } : { kind: "no-review" };
|
|
226
|
+
}
|
|
210
227
|
const latest = rounds[rounds.length - 1];
|
|
211
228
|
if (input.roundAfterId !== void 0 && (latest.id === void 0 || latest.id <= input.roundAfterId)) {
|
|
212
|
-
return { kind: "no-round", commentBody: renderNoRoundNotice(input.locale, input.headSha) };
|
|
229
|
+
return reviewRan ? { kind: "no-summary", commentBody: renderNoSummaryNotice(input.locale, input.headSha) } : { kind: "no-round", commentBody: renderNoRoundNotice(input.locale, input.headSha) };
|
|
213
230
|
}
|
|
214
231
|
if (latest.extraction.status !== "ok") {
|
|
215
|
-
return { kind: "unparseable", commentBody: renderUnparseableNotice(input.locale, input.headSha) };
|
|
232
|
+
return { kind: "unparseable", commentBody: renderUnparseableNotice(input.locale, input.headSha, reviewRan), failClosed: reviewRan };
|
|
216
233
|
}
|
|
217
234
|
const verdict = computeVerdict(latest.extraction.findings);
|
|
218
235
|
const commentBody = renderVerdictComment({
|
|
@@ -231,7 +248,19 @@ var realExec = (command, args, stdin) => {
|
|
|
231
248
|
const result = spawnSync(command, args, { encoding: "utf-8", input: stdin });
|
|
232
249
|
return { status: result.status ?? null, stdout: result.stdout ?? "" };
|
|
233
250
|
};
|
|
234
|
-
|
|
251
|
+
var realWriteOutput = (path, text) => appendFileSync(path, text);
|
|
252
|
+
function outputModePath(env) {
|
|
253
|
+
return env["ARCHWEAVE_VERDICT_OUTPUT"] === "github-output" && env["GITHUB_OUTPUT"] ? env["GITHUB_OUTPUT"] : void 0;
|
|
254
|
+
}
|
|
255
|
+
function renderStepOutputs(commentBody, previousId, blocked) {
|
|
256
|
+
let delimiter;
|
|
257
|
+
do
|
|
258
|
+
delimiter = `ARCHWEAVE_${randomBytes(8).toString("hex")}`;
|
|
259
|
+
while (commentBody.split("\n").includes(delimiter));
|
|
260
|
+
return [`comment-body<<${delimiter}`, commentBody, delimiter, `previous-comment-id=${previousId ?? ""}`, `blocked=${blocked}`, ""].join("\n");
|
|
261
|
+
}
|
|
262
|
+
var UNREAD_COMMENTS_ERROR = "::error title=Review verdict not computed::Could not read PR comments, so the verdict could not be computed. Re-run the failed jobs to retry. If it fails again, check that the review job has pull-requests: read.";
|
|
263
|
+
function runReviewVerdictBin(env, exec, writeOutput = realWriteOutput) {
|
|
235
264
|
const logs = [];
|
|
236
265
|
const repo = env["GITHUB_REPOSITORY"];
|
|
237
266
|
const pr = env["ARCHWEAVE_PR_NUMBER"];
|
|
@@ -245,41 +274,54 @@ function runReviewVerdictBin(env, exec) {
|
|
|
245
274
|
const roundAfterId = watermark !== void 0 && /^\d+$/.test(watermark) ? Number(watermark) : void 0;
|
|
246
275
|
const sha = env["ARCHWEAVE_HEAD_SHA"];
|
|
247
276
|
const headSha = sha !== void 0 && /^[0-9a-f]{7,40}$/i.test(sha) ? sha : void 0;
|
|
277
|
+
const outputPath = outputModePath(env);
|
|
248
278
|
const fetched = exec("gh", ["api", `repos/${repo}/issues/${pr}/comments`, "--paginate"]);
|
|
249
|
-
|
|
250
|
-
logs.push("verdict gate: could not fetch PR comments via gh \u2014 skipping (check stays green).");
|
|
251
|
-
return { exitCode: 0, logs };
|
|
252
|
-
}
|
|
253
|
-
const pages = splitConcatenatedJson(fetched.stdout);
|
|
279
|
+
const pages = fetched.status === 0 ? splitConcatenatedJson(fetched.stdout) : null;
|
|
254
280
|
if (pages === null) {
|
|
255
|
-
|
|
281
|
+
if (outputPath !== void 0) return { exitCode: 1, logs: [UNREAD_COMMENTS_ERROR] };
|
|
282
|
+
logs.push(
|
|
283
|
+
fetched.status !== 0 ? "verdict gate: could not fetch PR comments via gh \u2014 skipping (check stays green)." : "verdict gate: unexpected gh output \u2014 skipping (check stays green)."
|
|
284
|
+
);
|
|
256
285
|
return { exitCode: 0, logs };
|
|
257
286
|
}
|
|
258
287
|
const comments = pages.flatMap((page) => Array.isArray(page) ? page : []).filter(
|
|
259
288
|
(c) => c !== null && typeof c === "object" && typeof c.body === "string" && c.user?.type === "Bot"
|
|
260
289
|
).map((c) => ({ body: c.body, id: typeof c.id === "number" ? c.id : void 0 }));
|
|
261
|
-
const
|
|
290
|
+
const reviewRan = outputPath !== void 0 && roundAfterId !== void 0;
|
|
291
|
+
const result = runVerdictGate({ comments, maxRounds, locale, roundAfterId, headSha, reviewRan });
|
|
262
292
|
if (result.kind === "no-review") {
|
|
263
293
|
logs.push("verdict gate: no review rounds found on this PR \u2014 nothing to judge.");
|
|
264
294
|
return { exitCode: 0, logs };
|
|
265
295
|
}
|
|
266
296
|
const previous = [...comments].reverse().find((c) => c.body.includes(VERDICT_MARKER) && c.id !== void 0);
|
|
267
|
-
const
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
297
|
+
const failedReview = result.kind === "no-summary" || result.kind === "unparseable" && result.failClosed;
|
|
298
|
+
const settled = !failedReview && (result.kind === "no-round" || result.kind === "unparseable");
|
|
299
|
+
const blocked = failedReview || result.kind === "verdict" && result.exitCode !== 0;
|
|
300
|
+
if (outputPath !== void 0) {
|
|
301
|
+
writeOutput(outputPath, renderStepOutputs(result.commentBody, previous?.id, blocked));
|
|
302
|
+
} else {
|
|
303
|
+
const payload = JSON.stringify({ body: result.commentBody });
|
|
304
|
+
const updated = previous !== void 0 && exec("gh", ["api", "-X", "PATCH", `repos/${repo}/issues/comments/${previous.id}`, "--input", "-"], payload).status === 0;
|
|
305
|
+
if (!updated) {
|
|
306
|
+
const post = exec("gh", ["api", "-X", "POST", `repos/${repo}/issues/${pr}/comments`, "--input", "-"], payload);
|
|
307
|
+
if (post.status !== 0) logs.push("verdict gate: could not post the verdict comment (continuing).");
|
|
308
|
+
}
|
|
272
309
|
}
|
|
273
|
-
if (
|
|
274
|
-
logs.push(
|
|
310
|
+
if (failedReview) {
|
|
311
|
+
logs.push(
|
|
312
|
+
result.kind === "no-summary" ? "::error title=AI review posted no summary::The AI review ran but posted no summary for this commit, so there is no verdict. Review the PR manually, or re-run all jobs." : "::error title=AI review summary unreadable::The AI review summary has no readable findings block, so there is no verdict. Review the PR manually, or re-run all jobs."
|
|
313
|
+
);
|
|
275
314
|
return { exitCode: 0, logs };
|
|
276
315
|
}
|
|
277
|
-
if (
|
|
278
|
-
logs.push(
|
|
316
|
+
if (settled) {
|
|
317
|
+
logs.push(
|
|
318
|
+
result.kind === "no-round" ? "verdict gate: this run posted no review round for the current commit \u2014 earlier rounds are not judged again; check stays green." : "verdict gate: review output was not machine-readable \u2014 human review requested, check stays green."
|
|
319
|
+
);
|
|
279
320
|
return { exitCode: 0, logs };
|
|
280
321
|
}
|
|
322
|
+
if (result.kind !== "verdict") return { exitCode: 0, logs };
|
|
281
323
|
logs.push(`verdict gate: ${result.verdict}`);
|
|
282
|
-
return { exitCode: result.exitCode, logs };
|
|
324
|
+
return { exitCode: outputPath === void 0 ? result.exitCode : 0, logs };
|
|
283
325
|
}
|
|
284
326
|
function splitConcatenatedJson(stdout) {
|
|
285
327
|
const text = stdout.trim();
|