@clearlydev/nx-archweave 0.2.2 → 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 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 structure with enforced module boundaries
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, Gemini, GPT, Cursor)
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 self-contained bundle. Its only runtime dependency is `@nx/devkit`.
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
 
@@ -150,15 +150,29 @@ var NO_ROUND_NOTICE = {
150
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.",
151
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."
152
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
+ }
153
164
  var HEAD_LINE = {
154
- en: (sha) => `<sub>Commit \`${sha.slice(0, 7)}\` \xB7 this comment is updated on every review round.</sub>`,
155
- ko: (sha) => `<sub>\uCEE4\uBC0B \`${sha.slice(0, 7)}\` \xB7 \uC774 \uCF54\uBA58\uD2B8\uB294 \uB9AC\uBDF0 \uB77C\uC6B4\uB4DC\uB9C8\uB2E4 \uAC31\uC2E0\uB429\uB2C8\uB2E4.</sub>`
165
+ en: (sha) => renderCommitLine("en", sha.slice(0, 7)),
166
+ ko: (sha) => renderCommitLine("ko", sha.slice(0, 7))
156
167
  };
157
168
  function withHead(lines, locale, headSha) {
158
169
  return (headSha === void 0 ? lines : [...lines, "", HEAD_LINE[locale](headSha)]).join("\n");
159
170
  }
160
- function renderUnparseableNotice(locale, headSha) {
161
- 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);
162
176
  }
163
177
  function renderNoRoundNotice(locale, headSha) {
164
178
  return withHead([VERDICT_MARKER, NO_ROUND_NOTICE[locale]], locale, headSha);
@@ -206,14 +220,16 @@ function computeRoundHistory(rounds) {
206
220
  }
207
221
  function runVerdictGate(input) {
208
222
  const rounds = collectRounds(input.comments);
209
- if (rounds.length === 0)
210
- return { kind: "no-review" };
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
+ }
211
227
  const latest = rounds[rounds.length - 1];
212
228
  if (input.roundAfterId !== void 0 && (latest.id === void 0 || latest.id <= input.roundAfterId)) {
213
- 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) };
214
230
  }
215
231
  if (latest.extraction.status !== "ok") {
216
- return { kind: "unparseable", commentBody: renderUnparseableNotice(input.locale, input.headSha) };
232
+ return { kind: "unparseable", commentBody: renderUnparseableNotice(input.locale, input.headSha, reviewRan), failClosed: reviewRan };
217
233
  }
218
234
  const verdict = computeVerdict(latest.extraction.findings);
219
235
  const commentBody = renderVerdictComment({
@@ -271,15 +287,18 @@ function runReviewVerdictBin(env, exec, writeOutput = realWriteOutput) {
271
287
  const comments = pages.flatMap((page) => Array.isArray(page) ? page : []).filter(
272
288
  (c) => c !== null && typeof c === "object" && typeof c.body === "string" && c.user?.type === "Bot"
273
289
  ).map((c) => ({ body: c.body, id: typeof c.id === "number" ? c.id : void 0 }));
274
- const result = runVerdictGate({ comments, maxRounds, locale, roundAfterId, headSha });
290
+ const reviewRan = outputPath !== void 0 && roundAfterId !== void 0;
291
+ const result = runVerdictGate({ comments, maxRounds, locale, roundAfterId, headSha, reviewRan });
275
292
  if (result.kind === "no-review") {
276
293
  logs.push("verdict gate: no review rounds found on this PR \u2014 nothing to judge.");
277
294
  return { exitCode: 0, logs };
278
295
  }
279
296
  const previous = [...comments].reverse().find((c) => c.body.includes(VERDICT_MARKER) && c.id !== void 0);
280
- const settled = result.kind === "no-round" || result.kind === "unparseable";
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;
281
300
  if (outputPath !== void 0) {
282
- writeOutput(outputPath, renderStepOutputs(result.commentBody, previous?.id, !settled && result.exitCode !== 0));
301
+ writeOutput(outputPath, renderStepOutputs(result.commentBody, previous?.id, blocked));
283
302
  } else {
284
303
  const payload = JSON.stringify({ body: result.commentBody });
285
304
  const updated = previous !== void 0 && exec("gh", ["api", "-X", "PATCH", `repos/${repo}/issues/comments/${previous.id}`, "--input", "-"], payload).status === 0;
@@ -288,12 +307,19 @@ function runReviewVerdictBin(env, exec, writeOutput = realWriteOutput) {
288
307
  if (post.status !== 0) logs.push("verdict gate: could not post the verdict comment (continuing).");
289
308
  }
290
309
  }
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
+ );
314
+ return { exitCode: 0, logs };
315
+ }
291
316
  if (settled) {
292
317
  logs.push(
293
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."
294
319
  );
295
320
  return { exitCode: 0, logs };
296
321
  }
322
+ if (result.kind !== "verdict") return { exitCode: 0, logs };
297
323
  logs.push(`verdict gate: ${result.verdict}`);
298
324
  return { exitCode: outputPath === void 0 ? result.exitCode : 0, logs };
299
325
  }