@clearlydev/nx-archweave 0.3.1 → 0.3.3
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/dist/bin/review-verdict.js +5 -248
- package/dist/eslint/index.js +28 -321
- package/dist/generators/_chunks/chunk-2WSX5YNB.js +274 -0
- package/dist/generators/_chunks/chunk-34V6HAJT.js +55 -0
- package/dist/generators/_chunks/chunk-3A6RZUCZ.js +114 -0
- package/dist/generators/_chunks/chunk-47T7NOPP.js +30 -0
- package/dist/generators/_chunks/chunk-5MI3O4VZ.js +99 -0
- package/dist/generators/_chunks/chunk-67INEVWL.js +1068 -0
- package/dist/generators/_chunks/chunk-6ZTIUBB2.js +139 -0
- package/dist/generators/_chunks/chunk-AIJCJ7JK.js +203 -0
- package/dist/generators/_chunks/chunk-AQHV7DJS.js +656 -0
- package/dist/generators/_chunks/chunk-C2VVUWBA.js +386 -0
- package/dist/generators/_chunks/chunk-EXYZLL7A.js +234 -0
- package/dist/generators/_chunks/chunk-FQHFT5IH.js +50 -0
- package/dist/generators/_chunks/chunk-H222FPLK.js +80 -0
- package/dist/generators/_chunks/chunk-IIDOBI42.js +616 -0
- package/dist/generators/_chunks/chunk-JHRTVURZ.js +189 -0
- package/dist/generators/_chunks/chunk-L2RZ2LBO.js +132 -0
- package/dist/generators/_chunks/chunk-MQNQJOWA.js +217 -0
- package/dist/generators/_chunks/chunk-QQNDYJTR.js +243 -0
- package/dist/generators/_chunks/chunk-SFXU6IUS.js +0 -0
- package/dist/generators/_chunks/chunk-SNBIVIMI.js +89 -0
- package/dist/generators/_chunks/chunk-TUN34LAQ.js +8 -0
- package/dist/generators/_chunks/chunk-TXAQ2GJC.js +20 -0
- package/dist/generators/_chunks/chunk-U6LAFHSF.js +49 -0
- package/dist/generators/_chunks/chunk-URV6FLUH.js +73 -0
- package/dist/generators/_chunks/chunk-YNMLB4DL.js +25 -0
- package/dist/generators/_chunks/chunk-YY2UESLM.js +282 -0
- package/dist/generators/add-agent-rules/generator.js +11 -842
- package/dist/generators/add-backend/generator.js +11 -906
- package/dist/generators/add-fsd-lite/generator.js +12 -801
- package/dist/generators/add-github-templates/generator.js +13 -116
- package/dist/generators/add-integration/generator.js +15 -743
- package/dist/generators/add-review/generator.js +12 -1231
- package/dist/generators/allow-builds/generator.js +7 -71
- package/dist/generators/doctor/generator.js +74 -2042
- package/dist/generators/entity/generator.js +15 -604
- package/dist/generators/feature/generator.js +15 -604
- package/dist/generators/init/generator.js +68 -3376
- package/dist/generators/install-integration/generator.js +35 -871
- package/dist/generators/page/generator.js +15 -604
- package/dist/generators/record-integration/generator.js +12 -778
- package/dist/generators/shared/generator.js +15 -604
- package/dist/generators/widget/generator.js +15 -604
- package/dist/index.js +5 -663
- package/package.json +2 -2
|
@@ -1,1921 +1,73 @@
|
|
|
1
|
+
import {
|
|
2
|
+
recordedReview,
|
|
3
|
+
runAddReview
|
|
4
|
+
} from "../_chunks/chunk-JHRTVURZ.js";
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_MAX_ROUNDS,
|
|
7
|
+
buildReviewConfigSnippet
|
|
8
|
+
} from "../_chunks/chunk-67INEVWL.js";
|
|
9
|
+
import {
|
|
10
|
+
renderAddAgentRulesReport,
|
|
11
|
+
runAddAgentRules
|
|
12
|
+
} from "../_chunks/chunk-IIDOBI42.js";
|
|
13
|
+
import {
|
|
14
|
+
renderAddReviewReport
|
|
15
|
+
} from "../_chunks/chunk-3A6RZUCZ.js";
|
|
16
|
+
import "../_chunks/chunk-5MI3O4VZ.js";
|
|
17
|
+
import {
|
|
18
|
+
renderAddBackendBody,
|
|
19
|
+
runAddBackend
|
|
20
|
+
} from "../_chunks/chunk-C2VVUWBA.js";
|
|
21
|
+
import "../_chunks/chunk-QQNDYJTR.js";
|
|
22
|
+
import {
|
|
23
|
+
DEFAULT_CONFLICT_POLICY,
|
|
24
|
+
renderAddFsdLiteReport,
|
|
25
|
+
runAddFsdLite
|
|
26
|
+
} from "../_chunks/chunk-YY2UESLM.js";
|
|
27
|
+
import "../_chunks/chunk-L2RZ2LBO.js";
|
|
28
|
+
import {
|
|
29
|
+
allFrameworkProfiles,
|
|
30
|
+
resolveFrontendApp
|
|
31
|
+
} from "../_chunks/chunk-2WSX5YNB.js";
|
|
32
|
+
import "../_chunks/chunk-TXAQ2GJC.js";
|
|
33
|
+
import {
|
|
34
|
+
readBoundaryTags
|
|
35
|
+
} from "../_chunks/chunk-6ZTIUBB2.js";
|
|
36
|
+
import {
|
|
37
|
+
renderAddGithubTemplatesReport,
|
|
38
|
+
runAddGithubTemplates
|
|
39
|
+
} from "../_chunks/chunk-SNBIVIMI.js";
|
|
40
|
+
import "../_chunks/chunk-YNMLB4DL.js";
|
|
41
|
+
import {
|
|
42
|
+
resolveDryRun
|
|
43
|
+
} from "../_chunks/chunk-TUN34LAQ.js";
|
|
44
|
+
import {
|
|
45
|
+
spawnableCommand
|
|
46
|
+
} from "../_chunks/chunk-34V6HAJT.js";
|
|
47
|
+
import {
|
|
48
|
+
DEFAULT_AI_HOSTS,
|
|
49
|
+
RECORDED_BACKENDS,
|
|
50
|
+
RECORDED_FRONTENDS,
|
|
51
|
+
RECORDED_PRESETS,
|
|
52
|
+
TOOLKIT_CONFIG_PATH,
|
|
53
|
+
applyJsonEdits,
|
|
54
|
+
readToolkitConfigFile,
|
|
55
|
+
recordedChoice,
|
|
56
|
+
recordedFrontends,
|
|
57
|
+
resolveAiHostsOption,
|
|
58
|
+
resolveInitFrontend,
|
|
59
|
+
resolveLocaleOption,
|
|
60
|
+
reviewProviderFor,
|
|
61
|
+
treePackageManager
|
|
62
|
+
} from "../_chunks/chunk-EXYZLL7A.js";
|
|
63
|
+
import {
|
|
64
|
+
packageManagerCommands
|
|
65
|
+
} from "../_chunks/chunk-FQHFT5IH.js";
|
|
66
|
+
|
|
1
67
|
// src/generators/init/generator.ts
|
|
2
68
|
import { execFileSync } from "node:child_process";
|
|
3
69
|
import { tmpdir } from "node:os";
|
|
4
|
-
import { join
|
|
5
|
-
|
|
6
|
-
// ../shared/dist/lib/toolkit-config.js
|
|
7
|
-
function readFsdEnforcement(fsd) {
|
|
8
|
-
if (typeof fsd !== "object" || fsd === null)
|
|
9
|
-
return void 0;
|
|
10
|
-
const enforcement = fsd["enforcement"];
|
|
11
|
-
return enforcement === "error" || enforcement === "warn" ? enforcement : void 0;
|
|
12
|
-
}
|
|
13
|
-
var DEFAULT_CONFLICT_POLICY = "merge-proposal";
|
|
14
|
-
|
|
15
|
-
// ../shared/dist/lib/spawnable-command.js
|
|
16
|
-
import { statSync } from "node:fs";
|
|
17
|
-
import { win32 } from "node:path";
|
|
18
|
-
function isFile(path) {
|
|
19
|
-
try {
|
|
20
|
-
return statSync(path).isFile();
|
|
21
|
-
} catch {
|
|
22
|
-
return false;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
var DEFAULT_ENV = { platform: process.platform, env: process.env, isFile };
|
|
26
|
-
function envValue(env, name) {
|
|
27
|
-
const key = Object.keys(env).find((candidate) => candidate.toUpperCase() === name);
|
|
28
|
-
return key === void 0 ? void 0 : env[key];
|
|
29
|
-
}
|
|
30
|
-
function resolveWindowsCommand(command, environment) {
|
|
31
|
-
const exts = (envValue(environment.env, "PATHEXT") ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean);
|
|
32
|
-
const candidates = (base) => win32.extname(base) !== "" ? [base] : exts.map((ext) => base + ext);
|
|
33
|
-
if (win32.isAbsolute(command) || /[\\/]/.test(command)) {
|
|
34
|
-
return candidates(command).find(environment.isFile);
|
|
35
|
-
}
|
|
36
|
-
const dirs = (envValue(environment.env, "PATH") ?? "").split(";").filter(Boolean);
|
|
37
|
-
for (const dir of dirs) {
|
|
38
|
-
const found = candidates(win32.join(dir, command)).find(environment.isFile);
|
|
39
|
-
if (found !== void 0)
|
|
40
|
-
return found;
|
|
41
|
-
}
|
|
42
|
-
return void 0;
|
|
43
|
-
}
|
|
44
|
-
var META_CHARS = /([()\][%!^"`<>&|;, *?])/g;
|
|
45
|
-
var BIN_SHIM = /node_modules[\\/]\.bin[\\/][^\\/]+\.cmd$/i;
|
|
46
|
-
function escapeCmdArgument(arg, doubleEscape = false) {
|
|
47
|
-
let quoted = arg.replace(/(\\*)"/g, '$1$1\\"');
|
|
48
|
-
quoted = quoted.replace(/(\\*)$/, "$1$1");
|
|
49
|
-
const escaped = `"${quoted}"`.replace(META_CHARS, "^$1");
|
|
50
|
-
return doubleEscape ? escaped.replace(META_CHARS, "^$1") : escaped;
|
|
51
|
-
}
|
|
52
|
-
function spawnableCommand(command, args, environment = DEFAULT_ENV) {
|
|
53
|
-
if (environment.platform !== "win32")
|
|
54
|
-
return { command, args: [...args] };
|
|
55
|
-
const resolved = resolveWindowsCommand(command, environment);
|
|
56
|
-
if (resolved === void 0 || !/\.(cmd|bat)$/i.test(resolved))
|
|
57
|
-
return { command, args: [...args] };
|
|
58
|
-
const doubleEscape = BIN_SHIM.test(resolved);
|
|
59
|
-
const line = [resolved.replace(META_CHARS, "^$1"), ...args.map((arg) => escapeCmdArgument(arg, doubleEscape))].join(" ");
|
|
60
|
-
return {
|
|
61
|
-
command: envValue(environment.env, "COMSPEC") ?? "cmd.exe",
|
|
62
|
-
args: ["/d", "/s", "/c", `"${line}"`],
|
|
63
|
-
windowsVerbatimArguments: true
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// ../template-engine/dist/environment/package-manager.js
|
|
68
|
-
var LOCKFILES = [
|
|
69
|
-
["yarn", ["yarn.lock"]],
|
|
70
|
-
["bun", ["bun.lock", "bun.lockb"]],
|
|
71
|
-
["npm", ["package-lock.json"]]
|
|
72
|
-
];
|
|
73
|
-
function detectWorkspacePackageManager(exists, read = () => void 0) {
|
|
74
|
-
if (exists("pnpm-lock.yaml") || exists("pnpm-workspace.yaml"))
|
|
75
|
-
return "pnpm";
|
|
76
|
-
const found = LOCKFILES.filter(([, files]) => files.some((file) => exists(file))).map(([pm]) => pm);
|
|
77
|
-
if (found.length <= 1)
|
|
78
|
-
return found[0] ?? null;
|
|
79
|
-
const declared = /"packageManager"\s*:\s*"(npm|yarn|bun)@/.exec(read("package.json") ?? "")?.[1];
|
|
80
|
-
const named = found.find((pm) => pm === declared);
|
|
81
|
-
if (named)
|
|
82
|
-
return named;
|
|
83
|
-
return found.includes("npm") ? "npm" : found[0];
|
|
84
|
-
}
|
|
85
|
-
function detectYarnFlavor(exists, read) {
|
|
86
|
-
if (exists(".yarnrc.yml"))
|
|
87
|
-
return "berry";
|
|
88
|
-
if (/^__metadata:/m.test(read("yarn.lock") ?? ""))
|
|
89
|
-
return "berry";
|
|
90
|
-
const major = /"packageManager"\s*:\s*"yarn@(\d+)\./.exec(read("package.json") ?? "")?.[1];
|
|
91
|
-
return major !== void 0 && Number(major) >= 2 ? "berry" : "classic";
|
|
92
|
-
}
|
|
93
|
-
function packageManagerCommands(pm, flavor = "classic") {
|
|
94
|
-
switch (pm) {
|
|
95
|
-
case "pnpm":
|
|
96
|
-
return { addDevAtRoot: (spec) => `pnpm add -D -w ${spec}`, addAtRoot: (spec) => `pnpm add -w ${spec}`, runNx: "pnpm nx", install: "pnpm install", workspaceRegistration: "pnpm-workspace.yaml" };
|
|
97
|
-
case "yarn":
|
|
98
|
-
return {
|
|
99
|
-
addDevAtRoot: (spec) => flavor === "berry" ? `yarn add -D ${spec}` : `yarn add -D -W ${spec}`,
|
|
100
|
-
addAtRoot: (spec) => flavor === "berry" ? `yarn add ${spec}` : `yarn add -W ${spec}`,
|
|
101
|
-
runNx: "yarn nx",
|
|
102
|
-
install: "yarn install",
|
|
103
|
-
workspaceRegistration: "package.json workspaces"
|
|
104
|
-
};
|
|
105
|
-
case "bun":
|
|
106
|
-
return { addDevAtRoot: (spec) => `bun add -d ${spec}`, addAtRoot: (spec) => `bun add ${spec}`, runNx: "bun nx", install: "bun install", workspaceRegistration: "package.json workspaces" };
|
|
107
|
-
case "npm":
|
|
108
|
-
return { addDevAtRoot: (spec) => `npm install -D ${spec}`, addAtRoot: (spec) => `npm install ${spec}`, runNx: "npm exec nx --", install: "npm install", workspaceRegistration: "package.json workspaces" };
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// ../review-policy/dist/schema/review-policy-config.js
|
|
113
|
-
var DEFAULT_MAX_ROUNDS = 3;
|
|
114
|
-
|
|
115
|
-
// ../review-policy/dist/modes/review-modes.js
|
|
116
|
-
var REVIEW_MODES = {
|
|
117
|
-
fast: {
|
|
118
|
-
mode: "fast",
|
|
119
|
-
promptStyle: "fast-condensed",
|
|
120
|
-
includesCorrectnessSecurity: true,
|
|
121
|
-
includesOverEngineering: false
|
|
122
|
-
},
|
|
123
|
-
standard: {
|
|
124
|
-
mode: "standard",
|
|
125
|
-
promptStyle: "sectioned",
|
|
126
|
-
includesCorrectnessSecurity: true,
|
|
127
|
-
includesOverEngineering: false
|
|
128
|
-
},
|
|
129
|
-
thorough: {
|
|
130
|
-
mode: "thorough",
|
|
131
|
-
promptStyle: "sectioned",
|
|
132
|
-
includesCorrectnessSecurity: true,
|
|
133
|
-
includesOverEngineering: true
|
|
134
|
-
}
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
// ../review-policy/dist/verdict-gate/findings-block.js
|
|
138
|
-
var FINDINGS_MARKER = "<!-- archweave-findings-v1 -->";
|
|
139
|
-
var FINDINGS_SCHEMA_ID = "archweave/findings@1";
|
|
140
|
-
|
|
141
|
-
// ../review-policy/dist/prompts/prompt-shared.js
|
|
142
|
-
function renderFindingSchemaInstruction(locale, priorities) {
|
|
143
|
-
const intro = locale === "en" ? "For every finding, fill in every field of this schema:" : "\uBAA8\uB4E0 finding\uC5D0 \uB300\uD574 \uC544\uB798 \uC2A4\uD0A4\uB9C8\uC758 \uBAA8\uB4E0 \uD544\uB4DC\uB97C \uCC44\uC6B0\uC138\uC694:";
|
|
144
|
-
return [
|
|
145
|
-
intro,
|
|
146
|
-
`- priority: ${priorities.join(" | ")}`,
|
|
147
|
-
"- category: kebab-case category name",
|
|
148
|
-
"- title: one-line summary",
|
|
149
|
-
"- file: path of the affected file",
|
|
150
|
-
"- lineRange: { start, end? }",
|
|
151
|
-
"- evidence: what in the code proves the problem",
|
|
152
|
-
"- impact: what goes wrong and for whom",
|
|
153
|
-
"- reproductionCondition: the exact input/state that triggers it",
|
|
154
|
-
"- recommendation: the concrete fix",
|
|
155
|
-
"- confidence: confirmed | plausible"
|
|
156
|
-
].join("\n");
|
|
157
|
-
}
|
|
158
|
-
function renderFindingsBlockInstruction(locale) {
|
|
159
|
-
const lines = locale === "en" ? [
|
|
160
|
-
"At the END of the top-level summary comment, append the machine-readable findings block:",
|
|
161
|
-
`the literal marker line ${FINDINGS_MARKER}, then a fenced \`\`\`json block containing`,
|
|
162
|
-
`{ "schema": "${FINDINGS_SCHEMA_ID}", "findings": [ ... ] } with one entry per finding,`,
|
|
163
|
-
"using exactly the finding schema above. If there are no findings, still append the block with an empty findings array."
|
|
164
|
-
] : [
|
|
165
|
-
"top-level \uC694\uC57D \uCF54\uBA58\uD2B8\uC758 \uB9E8 \uB05D\uC5D0 \uAE30\uACC4 \uD310\uB3C5\uC6A9 findings \uBE14\uB85D\uC744 \uB367\uBD99\uC774\uC138\uC694:",
|
|
166
|
-
`\uB9AC\uD130\uB7F4 \uB9C8\uCEE4 \uC904 ${FINDINGS_MARKER} \uB2E4\uC74C\uC5D0 fenced \`\`\`json \uBE14\uB85D\uC73C\uB85C`,
|
|
167
|
-
`{ "schema": "${FINDINGS_SCHEMA_ID}", "findings": [ ... ] }\uB97C, finding\uB9C8\uB2E4 \uD55C \uC6D0\uC18C\uC529, \uC704 finding \uC2A4\uD0A4\uB9C8 \uADF8\uB300\uB85C \uC791\uC131\uD558\uC138\uC694.`,
|
|
168
|
-
"finding\uC774 \uC5C6\uC5B4\uB3C4 \uBE48 \uBC30\uC5F4\uB85C \uBE14\uB85D\uC744 \uBC18\uB4DC\uC2DC \uD3EC\uD568\uD558\uC138\uC694."
|
|
169
|
-
];
|
|
170
|
-
return lines.join("\n");
|
|
171
|
-
}
|
|
172
|
-
function renderDedupInstruction(locale) {
|
|
173
|
-
return locale === "en" ? "If multiple findings share the same root cause, merge them into a single finding instead of reporting duplicates." : "\uAC19\uC740 \uC6D0\uC778\uC5D0\uC11C \uB098\uC628 finding\uC774 \uC5EC\uB7EC \uAC1C\uB77C\uBA74 \uC911\uBCF5 \uBCF4\uACE0\uD558\uC9C0 \uB9D0\uACE0 \uD558\uB098\uC758 finding\uC73C\uB85C \uD569\uCCD0\uC11C \uBCF4\uACE0\uD558\uC138\uC694.";
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// ../review-policy/dist/prompts/correctness-security-prompt.js
|
|
177
|
-
var CORRECTNESS_SECURITY_PRIORITIES = ["P0", "P1", "P2", "Nit"];
|
|
178
|
-
var FAST_PRIORITIES = ["P0", "P1", "P2"];
|
|
179
|
-
function buildCorrectnessSecurityPrompt(locale, style = "sectioned") {
|
|
180
|
-
if (style === "fast-condensed") {
|
|
181
|
-
const fastBody = locale === "en" ? [
|
|
182
|
-
"You are a code reviewer focused on correctness and security.",
|
|
183
|
-
"",
|
|
184
|
-
"Review the provided diff and report only the most critical problems. Skip minor style and taste remarks entirely \u2014 report nothing below P2.",
|
|
185
|
-
"",
|
|
186
|
-
`Only report findings with priority ${FAST_PRIORITIES.join(", ")}. Do NOT report P3 (over-engineering) findings \u2014 they are out of scope here.`
|
|
187
|
-
] : [
|
|
188
|
-
"\uB2F9\uC2E0\uC740 \uC815\uD655\uC131(correctness)\uACFC \uBCF4\uC548(security)\uC5D0 \uC9D1\uC911\uD558\uB294 \uCF54\uB4DC \uB9AC\uBDF0\uC5B4\uC785\uB2C8\uB2E4.",
|
|
189
|
-
"",
|
|
190
|
-
"\uC8FC\uC5B4\uC9C4 diff\uC5D0\uC11C \uAC00\uC7A5 \uCE58\uBA85\uC801\uC778 \uBB38\uC81C\uB9CC \uBCF4\uACE0\uD558\uC138\uC694. \uC0AC\uC18C\uD55C \uC2A4\uD0C0\uC77C\xB7\uCDE8\uD5A5 \uC9C0\uC801\uC740 \uC644\uC804\uD788 \uC0DD\uB7B5\uD558\uACE0, P2 \uBBF8\uB9CC\uC740 \uBCF4\uACE0\uD558\uC9C0 \uB9C8\uC138\uC694.",
|
|
191
|
-
"",
|
|
192
|
-
`\uC6B0\uC120\uC21C\uC704 ${FAST_PRIORITIES.join(", ")}\uC5D0 \uD574\uB2F9\uD558\uB294 finding\uB9CC \uBCF4\uACE0\uD558\uC138\uC694. P3(\uACFC\uC789 \uC124\uACC4)\uB294 \uBCF4\uACE0\uD558\uC9C0 \uB9C8\uC138\uC694 \u2014 \uC774 \uB9AC\uBDF0\uC758 \uBC94\uC704 \uBC16\uC785\uB2C8\uB2E4.`
|
|
193
|
-
];
|
|
194
|
-
return [...fastBody, "", renderFindingSchemaInstruction(locale, FAST_PRIORITIES), "", renderDedupInstruction(locale)].join("\n");
|
|
195
|
-
}
|
|
196
|
-
const body = locale === "en" ? [
|
|
197
|
-
"You are a code reviewer focused on correctness and security.",
|
|
198
|
-
"",
|
|
199
|
-
'Review the provided diff and report findings in two explicitly separated top-level sections: "Correctness" and "Security". Never mix the two.',
|
|
200
|
-
"",
|
|
201
|
-
`Only report findings with priority ${CORRECTNESS_SECURITY_PRIORITIES.join(", ")}. Do NOT report P3 (over-engineering) findings \u2014 they are covered by a separate reviewer and are out of scope here.`
|
|
202
|
-
] : [
|
|
203
|
-
"\uB2F9\uC2E0\uC740 \uC815\uD655\uC131(correctness)\uACFC \uBCF4\uC548(security)\uC5D0 \uC9D1\uC911\uD558\uB294 \uCF54\uB4DC \uB9AC\uBDF0\uC5B4\uC785\uB2C8\uB2E4.",
|
|
204
|
-
"",
|
|
205
|
-
'\uC8FC\uC5B4\uC9C4 diff\uB97C \uB9AC\uBDF0\uD558\uACE0, finding\uC744 "Correctness"\uC640 "Security" \uB450 \uAC1C\uC758 \uCD5C\uC0C1\uC704 \uC139\uC158\uC73C\uB85C \uBD84\uB9AC\uD574 \uBCF4\uACE0\uD558\uC138\uC694. \uB450 \uC139\uC158\uC744 \uC808\uB300 \uC11E\uC9C0 \uB9C8\uC138\uC694.',
|
|
206
|
-
"",
|
|
207
|
-
`\uC6B0\uC120\uC21C\uC704 ${CORRECTNESS_SECURITY_PRIORITIES.join(", ")}\uC5D0 \uD574\uB2F9\uD558\uB294 finding\uB9CC \uBCF4\uACE0\uD558\uC138\uC694. P3\uB294 \uBCF4\uACE0\uD558\uC9C0 \uB9C8\uC138\uC694 \u2014 P3(\uACFC\uC789 \uC124\uACC4)\uB294 \uBCC4\uB3C4\uC758 \uB9AC\uBDF0\uC5B4\uAC00 \uB2F4\uB2F9\uD558\uBBC0\uB85C \uC774 \uB9AC\uBDF0\uC758 \uBC94\uC704 \uBC16\uC785\uB2C8\uB2E4.`
|
|
208
|
-
];
|
|
209
|
-
return [...body, "", renderFindingSchemaInstruction(locale, CORRECTNESS_SECURITY_PRIORITIES), "", renderDedupInstruction(locale)].join("\n");
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// ../review-policy/dist/prompts/over-engineering-prompt.js
|
|
213
|
-
var OVER_ENGINEERING_PRIORITIES = ["P3"];
|
|
214
|
-
function buildOverEngineeringPrompt(locale) {
|
|
215
|
-
const body = locale === "en" ? [
|
|
216
|
-
"You are a code reviewer focused exclusively on over-engineering.",
|
|
217
|
-
"",
|
|
218
|
-
"Review the provided diff and report findings with priority P3 only. Do NOT report P0, P1, P2, or Nit findings \u2014 correctness and security are covered by a separate reviewer.",
|
|
219
|
-
"",
|
|
220
|
-
'Report your findings in their own top-level section titled "Over-engineering"; never mix them into correctness or security sections.'
|
|
221
|
-
] : [
|
|
222
|
-
"\uB2F9\uC2E0\uC740 \uACFC\uC789 \uC124\uACC4(over-engineering)\uB9CC\uC744 \uC804\uB2F4\uD558\uB294 \uCF54\uB4DC \uB9AC\uBDF0\uC5B4\uC785\uB2C8\uB2E4.",
|
|
223
|
-
"",
|
|
224
|
-
"\uC8FC\uC5B4\uC9C4 diff\uB97C \uB9AC\uBDF0\uD558\uACE0 \uC6B0\uC120\uC21C\uC704 P3\uB9CC \uBCF4\uACE0\uD558\uC138\uC694. P0, P1, P2, Nit\uC740 \uBCF4\uACE0\uD558\uC9C0 \uB9C8\uC138\uC694 \u2014 \uC815\uD655\uC131\uACFC \uBCF4\uC548\uC740 \uBCC4\uB3C4\uC758 \uB9AC\uBDF0\uC5B4\uAC00 \uB2F4\uB2F9\uD569\uB2C8\uB2E4.",
|
|
225
|
-
"",
|
|
226
|
-
'finding\uC740 "Over-engineering"\uC774\uB77C\uB294 \uBCC4\uB3C4\uC758 \uCD5C\uC0C1\uC704 \uC139\uC158\uC73C\uB85C \uBCF4\uACE0\uD558\uACE0, correctness/security \uC139\uC158\uACFC \uC808\uB300 \uC11E\uC9C0 \uB9C8\uC138\uC694.'
|
|
227
|
-
];
|
|
228
|
-
return [...body, "", renderFindingSchemaInstruction(locale, OVER_ENGINEERING_PRIORITIES), "", renderDedupInstruction(locale)].join("\n");
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// ../review-policy/dist/verdict-gate/verdict-comment.js
|
|
232
|
-
var VERDICT_MARKER = "<!-- archweave-verdict -->";
|
|
233
|
-
function renderCommitLine(locale, shortSha) {
|
|
234
|
-
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>`;
|
|
235
|
-
}
|
|
236
|
-
var NOT_RUN_REASON = {
|
|
237
|
-
en: {
|
|
238
|
-
secret: (secretName) => `the ${secretName} secret is not registered`,
|
|
239
|
-
fork: () => "this pull request comes from a fork, and GitHub passes no secrets to runs from forks",
|
|
240
|
-
dependabot: (secretName) => `Dependabot runs receive only Dependabot secrets, and ${secretName} is not one of them`,
|
|
241
|
-
rounds: (_secretName, maxRounds) => `the automated review round limit (${maxRounds}) is reached`
|
|
242
|
-
},
|
|
243
|
-
ko: {
|
|
244
|
-
secret: (secretName) => `${secretName} \uC2DC\uD06C\uB9BF\uC774 \uB4F1\uB85D\uB418\uC9C0 \uC54A\uC74C`,
|
|
245
|
-
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",
|
|
246
|
-
dependabot: (secretName) => `Dependabot \uC2E4\uD589\uC740 Dependabot \uC2DC\uD06C\uB9BF\uB9CC \uBC1B\uB294\uB370, ${secretName}\uB294 \uADF8\uC911\uC5D0 \uC5C6\uC74C`,
|
|
247
|
-
rounds: (_secretName, maxRounds) => `\uC790\uB3D9 \uB9AC\uBDF0 \uB77C\uC6B4\uB4DC \uD55C\uB3C4(${maxRounds})\uC5D0 \uB3C4\uB2EC\uD568`
|
|
248
|
-
}
|
|
249
|
-
};
|
|
250
|
-
function renderNotRunNotice(locale, reason, secretName, maxRounds) {
|
|
251
|
-
const why = NOT_RUN_REASON[locale][reason](secretName, maxRounds);
|
|
252
|
-
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.`;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// ../review-policy/dist/verdict-gate/verdict-gate.js
|
|
256
|
-
var ROUND_MARKER = "<!-- archweave-review-round -->";
|
|
257
|
-
|
|
258
|
-
// ../template-engine/dist/review/config-snippet.js
|
|
259
|
-
function buildReviewConfigSnippet(input) {
|
|
260
|
-
return {
|
|
261
|
-
enabled: true,
|
|
262
|
-
provider: input.provider,
|
|
263
|
-
mode: input.mode,
|
|
264
|
-
locale: input.locale,
|
|
265
|
-
maxRounds: input.maxRounds,
|
|
266
|
-
blockingPriorities: ["P0", "P1"]
|
|
267
|
-
};
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// ../template-engine/dist/review/action-pins.js
|
|
271
|
-
var ACTION_PINS = {
|
|
272
|
-
"actions/checkout": "actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1",
|
|
273
|
-
"actions/setup-node": "actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0",
|
|
274
|
-
"pnpm/action-setup": "pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0",
|
|
275
|
-
"oven-sh/setup-bun": "oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0",
|
|
276
|
-
"anthropics/claude-code-action": "anthropics/claude-code-action@6b6870e649b5ff408753befeaee5fc7989d4e6a9 # v1.0.258",
|
|
277
|
-
"google-github-actions/run-gemini-cli": "google-github-actions/run-gemini-cli@f77273f4c914e4bf38440cf36a0369cb64a37489 # v0.1.22",
|
|
278
|
-
"openai/codex-action": "openai/codex-action@bdf19a4a223ec2549a3e2274a0cf61556bc07675 # v1.13"
|
|
279
|
-
};
|
|
280
|
-
function pinnedUses(action) {
|
|
281
|
-
return ACTION_PINS[action];
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// ../template-engine/dist/review/workflow-skeleton.js
|
|
285
|
-
function indentBlock(text, spaces) {
|
|
286
|
-
const pad = " ".repeat(spaces);
|
|
287
|
-
return text.split("\n").map((line) => line.length === 0 ? line : pad + line).join("\n");
|
|
288
|
-
}
|
|
289
|
-
function renderTriggerBlock() {
|
|
290
|
-
return ["on:", " pull_request:", " types: [opened, synchronize, ready_for_review, reopened]"].join("\n");
|
|
291
|
-
}
|
|
292
|
-
function renderConcurrencyBlock() {
|
|
293
|
-
return [
|
|
294
|
-
"concurrency:",
|
|
295
|
-
" group: archweave-review-${{ github.event.pull_request.number }}",
|
|
296
|
-
" cancel-in-progress: false"
|
|
297
|
-
].join("\n");
|
|
298
|
-
}
|
|
299
|
-
var REVIEW_GATE_CONDITION = "steps.secret-gate.outputs.enabled == 'true' && steps.round-gate.outputs.allowed == 'true'";
|
|
300
|
-
function renderReviewPermissions(idToken) {
|
|
301
|
-
return ["permissions:", " contents: read", " pull-requests: write", ...idToken ? [" id-token: write"] : []].join("\n");
|
|
302
|
-
}
|
|
303
|
-
function renderCheckoutStep(gateCondition, extraWith = []) {
|
|
304
|
-
return [
|
|
305
|
-
"- name: Checkout repository",
|
|
306
|
-
...gateCondition === void 0 ? [] : [` if: ${gateCondition}`],
|
|
307
|
-
` uses: ${pinnedUses("actions/checkout")}`,
|
|
308
|
-
" with:",
|
|
309
|
-
...extraWith.map((line) => ` ${line}`),
|
|
310
|
-
" fetch-depth: 1",
|
|
311
|
-
" persist-credentials: false"
|
|
312
|
-
].join("\n");
|
|
313
|
-
}
|
|
314
|
-
function renderSecretGateStep(secretName) {
|
|
315
|
-
return [
|
|
316
|
-
"- name: Check review secret",
|
|
317
|
-
" id: secret-gate",
|
|
318
|
-
" env:",
|
|
319
|
-
` REVIEW_SECRET: \${{ secrets.${secretName} }}`,
|
|
320
|
-
" run: |",
|
|
321
|
-
' if [ -z "$REVIEW_SECRET" ]; then',
|
|
322
|
-
` echo "AI review is not active: register the ${secretName} secret to enable it."`,
|
|
323
|
-
' echo "enabled=false" >> "$GITHUB_OUTPUT"',
|
|
324
|
-
" else",
|
|
325
|
-
' echo "enabled=true" >> "$GITHUB_OUTPUT"',
|
|
326
|
-
" fi"
|
|
327
|
-
].join("\n");
|
|
328
|
-
}
|
|
329
|
-
function renderRoundGateStep(maxRounds) {
|
|
330
|
-
return [
|
|
331
|
-
"- name: Check review round limit",
|
|
332
|
-
" id: round-gate",
|
|
333
|
-
" if: steps.secret-gate.outputs.enabled == 'true'",
|
|
334
|
-
" env:",
|
|
335
|
-
" GH_TOKEN: ${{ github.token }}",
|
|
336
|
-
" run: |",
|
|
337
|
-
` counts=$(gh api "repos/\${{ github.repository }}/issues/\${{ github.event.pull_request.number }}/comments" --paginate --jq '[([.[] | select(.user.type == "Bot") | select(.body | contains("${ROUND_MARKER}"))] | length), ([.[].id] | max // 0)]' | jq -s -r '[(map(.[0]) | add // 0), (map(.[1]) | max // 0)] | @tsv')`,
|
|
338
|
-
` rounds=$(printf '%s' "$counts" | cut -f1)`,
|
|
339
|
-
` echo "last-comment-id=$(printf '%s' "$counts" | cut -f2)" >> "$GITHUB_OUTPUT"`,
|
|
340
|
-
` if [ "$rounds" -ge ${maxRounds} ]; then`,
|
|
341
|
-
` echo "Automated review round limit (${maxRounds}) reached for this PR; skipping."`,
|
|
342
|
-
' echo "allowed=false" >> "$GITHUB_OUTPUT"',
|
|
343
|
-
" else",
|
|
344
|
-
' echo "allowed=true" >> "$GITHUB_OUTPUT"',
|
|
345
|
-
" fi"
|
|
346
|
-
].join("\n");
|
|
347
|
-
}
|
|
348
|
-
function shellQuote(text) {
|
|
349
|
-
return `'${text.replace(/'/g, `'\\''`)}'`;
|
|
350
|
-
}
|
|
351
|
-
function renderNotRunFunction(secretName, maxRounds, locale) {
|
|
352
|
-
const reasons = ["secret", "fork", "dependabot", "rounds"];
|
|
353
|
-
const [commitBefore, commitAfter] = renderCommitLine(locale, "\0").split("\0");
|
|
354
|
-
return [
|
|
355
|
-
"not_run() {",
|
|
356
|
-
' case "$1" in',
|
|
357
|
-
...reasons.map((reason) => ` ${reason}) annotation=${shellQuote(renderNotRunNotice("en", reason, secretName, maxRounds))}; notice=${shellQuote(renderNotRunNotice(locale, reason, secretName, maxRounds))} ;;`),
|
|
358
|
-
" esac",
|
|
359
|
-
' echo "::notice title=AI review did not run::$annotation"',
|
|
360
|
-
` printf '%s\\n' "$notice" >> "$GITHUB_STEP_SUMMARY"`,
|
|
361
|
-
` short_sha=$(printf '%s' "$HEAD_SHA" | cut -c1-7)`,
|
|
362
|
-
' case "$short_sha" in',
|
|
363
|
-
" '' | *[!0-9a-f]*) short_sha= ;;",
|
|
364
|
-
" esac",
|
|
365
|
-
" {",
|
|
366
|
-
' echo "notice-body<<ARCHWEAVE_NOT_RUN_EOF"',
|
|
367
|
-
` printf '%s\\n' ${shellQuote(VERDICT_MARKER)} "\u2139\uFE0F $notice"`,
|
|
368
|
-
` if [ -n "$short_sha" ]; then printf '\\n%s\\n' ${shellQuote(commitBefore)}"$short_sha"${shellQuote(commitAfter)}; fi`,
|
|
369
|
-
' echo "ARCHWEAVE_NOT_RUN_EOF"',
|
|
370
|
-
' echo "run=false"',
|
|
371
|
-
' } >> "$GITHUB_OUTPUT"',
|
|
372
|
-
"}"
|
|
373
|
-
];
|
|
374
|
-
}
|
|
375
|
-
var JUDGE_CONDITION = "steps.ai-review-result.outputs.run == 'true'";
|
|
376
|
-
function renderAiReviewResultStep(secretName, maxRounds, locale) {
|
|
377
|
-
return [
|
|
378
|
-
"- name: Check the AI review result",
|
|
379
|
-
" id: ai-review-result",
|
|
380
|
-
" env:",
|
|
381
|
-
" AI_REVIEW_RESULT: ${{ needs.ai-review.result }}",
|
|
382
|
-
" REVIEW_ENABLED: ${{ needs.ai-review.outputs.enabled }}",
|
|
383
|
-
" REVIEW_ALLOWED: ${{ needs.ai-review.outputs.allowed }}",
|
|
384
|
-
" LAST_COMMENT_ID: ${{ needs.ai-review.outputs.last-comment-id }}",
|
|
385
|
-
" HEAD_SHA: ${{ github.event.pull_request.head.sha }}",
|
|
386
|
-
" HEAD_REPOSITORY: ${{ github.event.pull_request.head.repo.full_name }}",
|
|
387
|
-
" REPOSITORY: ${{ github.repository }}",
|
|
388
|
-
" PR_AUTHOR: ${{ github.event.pull_request.user.login }}",
|
|
389
|
-
" run: |",
|
|
390
|
-
...renderNotRunFunction(secretName, maxRounds, locale).map((line) => ` ${line}`),
|
|
391
|
-
' if [ "$AI_REVIEW_RESULT" != success ]; then',
|
|
392
|
-
` echo "::error title=AI review did not succeed::The AI review job ended with '$AI_REVIEW_RESULT'."`,
|
|
393
|
-
" exit 1",
|
|
394
|
-
" fi",
|
|
395
|
-
' case "$REVIEW_ENABLED/$REVIEW_ALLOWED" in',
|
|
396
|
-
" false/*)",
|
|
397
|
-
' if [ "$HEAD_REPOSITORY" != "$REPOSITORY" ]; then not_run fork',
|
|
398
|
-
` elif [ "$PR_AUTHOR" = 'dependabot[bot]' ]; then not_run dependabot`,
|
|
399
|
-
" else not_run secret",
|
|
400
|
-
" fi",
|
|
401
|
-
" exit 0",
|
|
402
|
-
" ;;",
|
|
403
|
-
" true/false)",
|
|
404
|
-
" not_run rounds",
|
|
405
|
-
" exit 0",
|
|
406
|
-
" ;;",
|
|
407
|
-
" true/true)",
|
|
408
|
-
' case "$LAST_COMMENT_ID" in',
|
|
409
|
-
" '' | *[!0-9]*) ;;",
|
|
410
|
-
" *)",
|
|
411
|
-
' echo "run=true" >> "$GITHUB_OUTPUT"',
|
|
412
|
-
" exit 0",
|
|
413
|
-
" ;;",
|
|
414
|
-
" esac",
|
|
415
|
-
" ;;",
|
|
416
|
-
" esac",
|
|
417
|
-
` 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."`,
|
|
418
|
-
" exit 1"
|
|
419
|
-
].join("\n");
|
|
420
|
-
}
|
|
421
|
-
function renderVerdictGateStep(maxRounds, locale) {
|
|
422
|
-
return [
|
|
423
|
-
"- name: Compute review verdict",
|
|
424
|
-
" id: verdict",
|
|
425
|
-
` if: ${JUDGE_CONDITION}`,
|
|
426
|
-
" env:",
|
|
427
|
-
" GH_TOKEN: ${{ github.token }}",
|
|
428
|
-
" GITHUB_REPOSITORY: ${{ github.repository }}",
|
|
429
|
-
" ARCHWEAVE_PR_NUMBER: ${{ github.event.pull_request.number }}",
|
|
430
|
-
` ARCHWEAVE_MAX_ROUNDS: '${maxRounds}'`,
|
|
431
|
-
` ARCHWEAVE_LOCALE: '${locale}'`,
|
|
432
|
-
" ARCHWEAVE_HEAD_SHA: ${{ github.event.pull_request.head.sha }}",
|
|
433
|
-
" ARCHWEAVE_ROUND_AFTER_ID: ${{ needs.ai-review.outputs.last-comment-id }}",
|
|
434
|
-
" ARCHWEAVE_VERDICT_OUTPUT: 'github-output'",
|
|
435
|
-
" run: |",
|
|
436
|
-
" if [ -x node_modules/.bin/archweave-review-verdict ]; then",
|
|
437
|
-
" node_modules/.bin/archweave-review-verdict",
|
|
438
|
-
" else",
|
|
439
|
-
' echo "nx-archweave is not installed in this workspace - skipping the verdict gate."',
|
|
440
|
-
" fi"
|
|
441
|
-
].join("\n");
|
|
442
|
-
}
|
|
443
|
-
function renderBlockStep() {
|
|
444
|
-
return [
|
|
445
|
-
"- name: Fail the review check",
|
|
446
|
-
" if: steps.verdict.outputs.blocked == 'true'",
|
|
447
|
-
" env:",
|
|
448
|
-
" BLOCK_REASON: ${{ steps.verdict.outputs.block-reason }}",
|
|
449
|
-
" run: |",
|
|
450
|
-
' case "$BLOCK_REASON" in',
|
|
451
|
-
' block) echo "The AI verdict is BLOCK: fix the P0/P1 findings in the verdict comment and push." ;;',
|
|
452
|
-
' no-summary) echo "The AI review posted no summary for this commit, so there is no verdict. Re-run all jobs to review it again, or review it yourself." ;;',
|
|
453
|
-
' unreadable) echo "The AI review summary has no readable findings block, so there is no verdict. Re-run all jobs to review it again, or review it yourself." ;;',
|
|
454
|
-
' *) echo "The review check failed. See the verdict comment on this pull request." ;;',
|
|
455
|
-
" esac",
|
|
456
|
-
" exit 1"
|
|
457
|
-
].join("\n");
|
|
458
|
-
}
|
|
459
|
-
function renderVerdictCommentStep() {
|
|
460
|
-
return [
|
|
461
|
-
"- name: Post review verdict",
|
|
462
|
-
" env:",
|
|
463
|
-
" GH_TOKEN: ${{ github.token }}",
|
|
464
|
-
" REPOSITORY: ${{ github.repository }}",
|
|
465
|
-
" PR_NUMBER: ${{ github.event.pull_request.number }}",
|
|
466
|
-
" VERDICT_BODY: ${{ needs.review.outputs.comment-body }}",
|
|
467
|
-
" PREVIOUS_ID: ${{ needs.review.outputs.previous-comment-id }}",
|
|
468
|
-
" run: |",
|
|
469
|
-
` payload=$(jq -n --arg body "$VERDICT_BODY" '{body: $body}')`,
|
|
470
|
-
' if [ -z "$PREVIOUS_ID" ]; then',
|
|
471
|
-
` 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=`,
|
|
472
|
-
" fi",
|
|
473
|
-
' case "$PREVIOUS_ID" in',
|
|
474
|
-
" ''|*[!0-9]*) PREVIOUS_ID= ;;",
|
|
475
|
-
" esac",
|
|
476
|
-
` if [ -n "$PREVIOUS_ID" ] && printf '%s' "$payload" | gh api -X PATCH "repos/$REPOSITORY/issues/comments/$PREVIOUS_ID" --input - > /dev/null; then`,
|
|
477
|
-
" exit 0",
|
|
478
|
-
" fi",
|
|
479
|
-
` printf '%s' "$payload" | gh api -X POST "repos/$REPOSITORY/issues/$PR_NUMBER/comments" --input - > /dev/null \\`,
|
|
480
|
-
' || echo "Could not post the verdict comment (continuing)."'
|
|
481
|
-
].join("\n");
|
|
482
|
-
}
|
|
483
|
-
var SETUP_NODE_VERSION = "22";
|
|
484
|
-
var FALLBACK_PNPM_VERSION = "10";
|
|
485
|
-
var INSTALL_LINES = {
|
|
486
|
-
pnpm: [" pnpm install --frozen-lockfile --ignore-scripts"],
|
|
487
|
-
npm: [" npm ci --ignore-scripts"],
|
|
488
|
-
yarn: [
|
|
489
|
-
" corepack enable yarn",
|
|
490
|
-
' 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'
|
|
491
|
-
],
|
|
492
|
-
bun: [" bun install --frozen-lockfile --ignore-scripts"]
|
|
493
|
-
};
|
|
494
|
-
function renderPnpmVersionStep(gate) {
|
|
495
|
-
return [
|
|
496
|
-
"- name: Detect pnpm version",
|
|
497
|
-
" id: pnpm-version",
|
|
498
|
-
gate,
|
|
499
|
-
" run: |",
|
|
500
|
-
` if [ -n "$(jq -r '.packageManager // empty' package.json)" ]; then`,
|
|
501
|
-
' echo "version=" >> "$GITHUB_OUTPUT"',
|
|
502
|
-
" else",
|
|
503
|
-
` echo "version=${FALLBACK_PNPM_VERSION}" >> "$GITHUB_OUTPUT"`,
|
|
504
|
-
" fi"
|
|
505
|
-
].join("\n");
|
|
506
|
-
}
|
|
507
|
-
function renderWorkspaceSetupSteps(packageManager) {
|
|
508
|
-
const gate = ` if: ${JUDGE_CONDITION}`;
|
|
509
|
-
const steps = [];
|
|
510
|
-
if (packageManager === "pnpm") {
|
|
511
|
-
steps.push(renderPnpmVersionStep(gate));
|
|
512
|
-
steps.push([
|
|
513
|
-
"- name: Set up pnpm",
|
|
514
|
-
gate,
|
|
515
|
-
` uses: ${pinnedUses("pnpm/action-setup")}`,
|
|
516
|
-
" with:",
|
|
517
|
-
" version: ${{ steps.pnpm-version.outputs.version }}"
|
|
518
|
-
].join("\n"));
|
|
519
|
-
}
|
|
520
|
-
if (packageManager === "bun") {
|
|
521
|
-
steps.push(["- name: Set up bun", gate, ` uses: ${pinnedUses("oven-sh/setup-bun")}`].join("\n"));
|
|
522
|
-
}
|
|
523
|
-
steps.push([
|
|
524
|
-
"- name: Set up Node",
|
|
525
|
-
gate,
|
|
526
|
-
` uses: ${pinnedUses("actions/setup-node")}`,
|
|
527
|
-
" with:",
|
|
528
|
-
` node-version: ${SETUP_NODE_VERSION}`,
|
|
529
|
-
// yarn: no setup-node cache — it would query the runner's global yarn 1 even in a
|
|
530
|
-
// berry project. Corepack (shipped with Node) provides the project's own yarn below.
|
|
531
|
-
// bun: setup-node has no bun cache. Node is still set up — nx and the verdict bin
|
|
532
|
-
// run on it.
|
|
533
|
-
...packageManager === "yarn" || packageManager === "bun" ? [] : [` cache: ${packageManager}`]
|
|
534
|
-
].join("\n"));
|
|
535
|
-
steps.push([
|
|
536
|
-
"- name: Install workspace dependencies",
|
|
537
|
-
gate,
|
|
538
|
-
" run: |",
|
|
539
|
-
...INSTALL_LINES[packageManager]
|
|
540
|
-
].join("\n"));
|
|
541
|
-
return steps.join("\n\n");
|
|
542
|
-
}
|
|
543
|
-
var REVIEW_JOB_TIMEOUT_MINUTES = 15;
|
|
544
|
-
function renderReviewWorkflow(input, parts) {
|
|
545
|
-
const aiReviewSteps = [renderSecretGateStep(parts.secretName), renderRoundGateStep(input.maxRounds), ...parts.reviewSteps];
|
|
546
|
-
const reviewSteps = [
|
|
547
|
-
renderAiReviewResultStep(parts.secretName, input.maxRounds, input.locale),
|
|
548
|
-
renderCheckoutStep(JUDGE_CONDITION),
|
|
549
|
-
renderWorkspaceSetupSteps(input.packageManager),
|
|
550
|
-
renderVerdictGateStep(input.maxRounds, input.locale),
|
|
551
|
-
renderBlockStep()
|
|
552
|
-
];
|
|
553
|
-
return [
|
|
554
|
-
`name: ${parts.name}`,
|
|
555
|
-
"",
|
|
556
|
-
renderTriggerBlock(),
|
|
557
|
-
"",
|
|
558
|
-
renderConcurrencyBlock(),
|
|
559
|
-
"",
|
|
560
|
-
"permissions: {}",
|
|
561
|
-
"",
|
|
562
|
-
"jobs:",
|
|
563
|
-
" ai-review:",
|
|
564
|
-
" runs-on: ubuntu-latest",
|
|
565
|
-
` timeout-minutes: ${REVIEW_JOB_TIMEOUT_MINUTES}`,
|
|
566
|
-
indentBlock(renderReviewPermissions(parts.idToken), 4),
|
|
567
|
-
" outputs:",
|
|
568
|
-
" enabled: ${{ steps.secret-gate.outputs.enabled }}",
|
|
569
|
-
" allowed: ${{ steps.round-gate.outputs.allowed }}",
|
|
570
|
-
" last-comment-id: ${{ steps.round-gate.outputs.last-comment-id }}",
|
|
571
|
-
" steps:",
|
|
572
|
-
indentBlock(aiReviewSteps.join("\n\n"), 6),
|
|
573
|
-
"",
|
|
574
|
-
" review:",
|
|
575
|
-
" needs: ai-review",
|
|
576
|
-
" if: always()",
|
|
577
|
-
" runs-on: ubuntu-latest",
|
|
578
|
-
` timeout-minutes: ${REVIEW_JOB_TIMEOUT_MINUTES}`,
|
|
579
|
-
" permissions:",
|
|
580
|
-
" contents: read",
|
|
581
|
-
" pull-requests: read",
|
|
582
|
-
" outputs:",
|
|
583
|
-
" comment-body: ${{ steps.verdict.outputs.comment-body || steps.ai-review-result.outputs.notice-body }}",
|
|
584
|
-
" previous-comment-id: ${{ steps.verdict.outputs.previous-comment-id }}",
|
|
585
|
-
" steps:",
|
|
586
|
-
indentBlock(reviewSteps.join("\n\n"), 6),
|
|
587
|
-
"",
|
|
588
|
-
" review-comment:",
|
|
589
|
-
" needs: review",
|
|
590
|
-
// Runs after a BLOCK failed the review job, too: the comment explains the red check.
|
|
591
|
-
" if: ${{ !cancelled() && needs.review.outputs.comment-body != '' }}",
|
|
592
|
-
" runs-on: ubuntu-latest",
|
|
593
|
-
` timeout-minutes: ${REVIEW_JOB_TIMEOUT_MINUTES}`,
|
|
594
|
-
" permissions:",
|
|
595
|
-
" pull-requests: write",
|
|
596
|
-
" steps:",
|
|
597
|
-
indentBlock(renderVerdictCommentStep(), 6),
|
|
598
|
-
""
|
|
599
|
-
].join("\n");
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
// ../template-engine/dist/review/claude/claude-provider.js
|
|
603
|
-
var SECRET_NAME = "CLAUDE_CODE_OAUTH_TOKEN";
|
|
604
|
-
var ALLOWED_TOOLS = "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*)";
|
|
605
|
-
var CLAUDE_STEP_ID = "claude-review";
|
|
606
|
-
var PR_CONTEXT = ["REPO: ${{ github.repository }}", "PR NUMBER: ${{ github.event.pull_request.number }}"];
|
|
607
|
-
function renderWorkflowInstructions(locale) {
|
|
608
|
-
const lines = locale === "en" ? [
|
|
609
|
-
...PR_CONTEXT,
|
|
610
|
-
"",
|
|
611
|
-
"Review the diff of this pull request using `gh pr diff <PR NUMBER>` and `gh pr view <PR NUMBER>`.",
|
|
612
|
-
"Post specific findings as inline comments with `mcp__github_inline_comment__create_inline_comment` (pass `confirmed: true`), and exactly one top-level summary comment.",
|
|
613
|
-
"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 `$(...)`.",
|
|
614
|
-
"Run each `gh` command on its own: no pipes to other programs, no `&&` chains.",
|
|
615
|
-
`Always include the literal marker ${ROUND_MARKER} in the top-level summary comment.`,
|
|
616
|
-
"If there are no findings that require action, post exactly one top-level comment saying so (and still include the marker).",
|
|
617
|
-
"Do not modify any code, and never approve or merge the pull request.",
|
|
618
|
-
"Write summaries and explanations in English; keep file names, function names, and identifiers verbatim."
|
|
619
|
-
] : [
|
|
620
|
-
...PR_CONTEXT,
|
|
621
|
-
"",
|
|
622
|
-
"\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.",
|
|
623
|
-
"\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.",
|
|
624
|
-
"\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.",
|
|
625
|
-
"`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.",
|
|
626
|
-
`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.`,
|
|
627
|
-
'\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).',
|
|
628
|
-
"\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).",
|
|
629
|
-
"\uC694\uC57D\uACFC \uC124\uBA85\uC740 \uD55C\uAD6D\uC5B4\uB85C \uC4F0\uB418, \uD30C\uC77C\uBA85\xB7\uD568\uC218\uBA85\xB7\uC2DD\uBCC4\uC790\uB294 \uC6D0\uBB38 \uADF8\uB300\uB85C \uC720\uC9C0\uD558\uC138\uC694."
|
|
630
|
-
];
|
|
631
|
-
return lines.join("\n");
|
|
632
|
-
}
|
|
633
|
-
function composePrompt(input) {
|
|
634
|
-
const mode = REVIEW_MODES[input.mode];
|
|
635
|
-
const sections = [renderWorkflowInstructions(input.locale), buildCorrectnessSecurityPrompt(input.locale, mode.promptStyle)];
|
|
636
|
-
if (mode.includesOverEngineering) {
|
|
637
|
-
sections.push(buildOverEngineeringPrompt(input.locale));
|
|
638
|
-
}
|
|
639
|
-
sections.push(renderFindingsBlockInstruction(input.locale));
|
|
640
|
-
return sections.join("\n\n---\n\n");
|
|
641
|
-
}
|
|
642
|
-
function renderDeniedToolsStep() {
|
|
643
|
-
return [
|
|
644
|
-
"- name: Show denied tool calls",
|
|
645
|
-
` if: always() && steps.${CLAUDE_STEP_ID}.outputs.execution_file != ''`,
|
|
646
|
-
" env:",
|
|
647
|
-
` EXECUTION_FILE: \${{ steps.${CLAUDE_STEP_ID}.outputs.execution_file }}`,
|
|
648
|
-
" run: |",
|
|
649
|
-
' [ -f "$EXECUTION_FILE" ] || exit 0',
|
|
650
|
-
` stop_token="archweave-$(od -An -N16 -tx1 /dev/urandom | tr -d ' \\n')"`,
|
|
651
|
-
' echo "::stop-commands::$stop_token"',
|
|
652
|
-
` jq -c '.[] | select(.type == "result") | ((.permission_denials[]? | {tool: .tool_name, command: ((.tool_input.command // "") | tostring | .[0:200])}), (select(.is_error == true) | {error: ((.result // "") | tostring | .[0:300])}))' "$EXECUTION_FILE" || echo "Could not read the execution file."`,
|
|
653
|
-
' echo "::$stop_token::"'
|
|
654
|
-
].join("\n");
|
|
655
|
-
}
|
|
656
|
-
function buildWorkflowYaml(input) {
|
|
657
|
-
return renderReviewWorkflow(input, {
|
|
658
|
-
name: "Claude Code Review",
|
|
659
|
-
secretName: SECRET_NAME,
|
|
660
|
-
// The Claude GitHub App signs in with the workflow's OIDC token (claude-code-action's FAQ).
|
|
661
|
-
idToken: true,
|
|
662
|
-
reviewSteps: [
|
|
663
|
-
renderCheckoutStep(REVIEW_GATE_CONDITION),
|
|
664
|
-
[
|
|
665
|
-
"- name: Run Claude review",
|
|
666
|
-
` id: ${CLAUDE_STEP_ID}`,
|
|
667
|
-
` if: ${REVIEW_GATE_CONDITION}`,
|
|
668
|
-
` uses: ${pinnedUses("anthropics/claude-code-action")}`,
|
|
669
|
-
" with:",
|
|
670
|
-
` claude_code_oauth_token: \${{ secrets.${SECRET_NAME} }}`,
|
|
671
|
-
" prompt: |",
|
|
672
|
-
indentBlock(composePrompt(input), 6),
|
|
673
|
-
" claude_args: |",
|
|
674
|
-
` --allowedTools "${ALLOWED_TOOLS}"`
|
|
675
|
-
].join("\n"),
|
|
676
|
-
renderDeniedToolsStep()
|
|
677
|
-
]
|
|
678
|
-
});
|
|
679
|
-
}
|
|
680
|
-
var claudeProvider = {
|
|
681
|
-
id: "claude",
|
|
682
|
-
workflowFileName: ".github/workflows/claude-code-review.yml",
|
|
683
|
-
requiredSecretName: SECRET_NAME,
|
|
684
|
-
buildWorkflowYaml
|
|
685
|
-
};
|
|
686
|
-
|
|
687
|
-
// ../template-engine/dist/review/cursor/cursor-provider.js
|
|
688
|
-
var SECRET_NAME2 = "CURSOR_API_KEY";
|
|
689
|
-
var PROMPT_HEREDOC_DELIMITER = "ARCHWEAVE_CURSOR_REVIEW_PROMPT";
|
|
690
|
-
function renderWorkflowInstructions2(locale) {
|
|
691
|
-
const lines = locale === "en" ? [
|
|
692
|
-
"Review the diff of this pull request using `gh pr diff` and `gh pr view`.",
|
|
693
|
-
"Post specific findings as inline comments and exactly one top-level summary comment.",
|
|
694
|
-
`Always include the literal marker ${ROUND_MARKER} in the top-level summary comment.`,
|
|
695
|
-
"If there are no findings that require action, post exactly one top-level comment saying so (and still include the marker).",
|
|
696
|
-
"Do not modify any code, and never approve or merge the pull request.",
|
|
697
|
-
"Write summaries and explanations in English; keep file names, function names, and identifiers verbatim."
|
|
698
|
-
] : [
|
|
699
|
-
"\uC774 pull request\uC758 diff\uB97C `gh pr diff`\uC640 `gh pr view`\uB85C \uD655\uC778\uD558\uACE0 \uB9AC\uBDF0\uD558\uC138\uC694.",
|
|
700
|
-
"\uAD6C\uCCB4\uC801 \uC9C0\uC801\uC740 inline comment\uB85C, \uC804\uCCB4 \uC694\uC57D\uC740 \uC815\uD655\uD788 \uD55C \uAC1C\uC758 top-level comment\uB85C \uAC8C\uC2DC\uD558\uC138\uC694.",
|
|
701
|
-
`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.`,
|
|
702
|
-
'\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).',
|
|
703
|
-
"\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).",
|
|
704
|
-
"\uC694\uC57D\uACFC \uC124\uBA85\uC740 \uD55C\uAD6D\uC5B4\uB85C \uC4F0\uB418, \uD30C\uC77C\uBA85\xB7\uD568\uC218\uBA85\xB7\uC2DD\uBCC4\uC790\uB294 \uC6D0\uBB38 \uADF8\uB300\uB85C \uC720\uC9C0\uD558\uC138\uC694."
|
|
705
|
-
];
|
|
706
|
-
return lines.join("\n");
|
|
707
|
-
}
|
|
708
|
-
function composePrompt2(input) {
|
|
709
|
-
const mode = REVIEW_MODES[input.mode];
|
|
710
|
-
const sections = [renderWorkflowInstructions2(input.locale), buildCorrectnessSecurityPrompt(input.locale, mode.promptStyle)];
|
|
711
|
-
if (mode.includesOverEngineering) {
|
|
712
|
-
sections.push(buildOverEngineeringPrompt(input.locale));
|
|
713
|
-
}
|
|
714
|
-
sections.push(renderFindingsBlockInstruction(input.locale));
|
|
715
|
-
return sections.join("\n\n---\n\n");
|
|
716
|
-
}
|
|
717
|
-
function buildWorkflowYaml2(input) {
|
|
718
|
-
return renderReviewWorkflow(input, {
|
|
719
|
-
name: "Cursor Code Review",
|
|
720
|
-
secretName: SECRET_NAME2,
|
|
721
|
-
// An API key: the Cursor CLI needs no OIDC token.
|
|
722
|
-
idToken: false,
|
|
723
|
-
reviewSteps: [
|
|
724
|
-
renderCheckoutStep(REVIEW_GATE_CONDITION),
|
|
725
|
-
[
|
|
726
|
-
"- name: Install cursor-agent CLI",
|
|
727
|
-
` if: ${REVIEW_GATE_CONDITION}`,
|
|
728
|
-
" run: |",
|
|
729
|
-
// HTTPS only, TLS 1.2 or newer. Cursor publishes no pinned version or checksum for
|
|
730
|
-
// this installer; docs/pr-review.md states what that leaves open.
|
|
731
|
-
" curl --proto '=https' --tlsv1.2 -fsS https://cursor.com/install | bash",
|
|
732
|
-
// The docs name ~/.cursor/bin; the installer currently links `agent` into ~/.local/bin.
|
|
733
|
-
// Both go on the PATH, so the next step finds it wherever the installer puts it.
|
|
734
|
-
' echo "$HOME/.local/bin" >> "$GITHUB_PATH"',
|
|
735
|
-
' echo "$HOME/.cursor/bin" >> "$GITHUB_PATH"'
|
|
736
|
-
].join("\n"),
|
|
737
|
-
[
|
|
738
|
-
"- name: Run Cursor review",
|
|
739
|
-
` if: ${REVIEW_GATE_CONDITION}`,
|
|
740
|
-
" env:",
|
|
741
|
-
` CURSOR_API_KEY: \${{ secrets.${SECRET_NAME2} }}`,
|
|
742
|
-
" GH_TOKEN: ${{ github.token }}",
|
|
743
|
-
" run: |",
|
|
744
|
-
// cursor-agent stops at "Workspace Trust Required" without --trust (2026.10.22 on
|
|
745
|
-
// Actions); the folder is this run's own checkout, and forks never get the secret.
|
|
746
|
-
` agent --trust -p "$(cat <<'${PROMPT_HEREDOC_DELIMITER}'`,
|
|
747
|
-
indentBlock(composePrompt2(input), 4),
|
|
748
|
-
` ${PROMPT_HEREDOC_DELIMITER}`,
|
|
749
|
-
' )"'
|
|
750
|
-
].join("\n")
|
|
751
|
-
]
|
|
752
|
-
});
|
|
753
|
-
}
|
|
754
|
-
var cursorProvider = {
|
|
755
|
-
id: "cursor",
|
|
756
|
-
workflowFileName: ".github/workflows/cursor-code-review.yml",
|
|
757
|
-
requiredSecretName: SECRET_NAME2,
|
|
758
|
-
buildWorkflowYaml: buildWorkflowYaml2
|
|
759
|
-
};
|
|
760
|
-
|
|
761
|
-
// ../template-engine/dist/review/gemini/gemini-provider.js
|
|
762
|
-
var SECRET_NAME3 = "GEMINI_API_KEY";
|
|
763
|
-
function renderWorkflowInstructions3(locale) {
|
|
764
|
-
const lines = locale === "en" ? [
|
|
765
|
-
"Review the diff of this pull request using `gh pr diff` and `gh pr view`.",
|
|
766
|
-
"Post specific findings as inline comments and exactly one top-level summary comment.",
|
|
767
|
-
`Always include the literal marker ${ROUND_MARKER} in the top-level summary comment.`,
|
|
768
|
-
"If there are no findings that require action, post exactly one top-level comment saying so (and still include the marker).",
|
|
769
|
-
"Do not modify any code, and never approve or merge the pull request.",
|
|
770
|
-
"Write summaries and explanations in English; keep file names, function names, and identifiers verbatim."
|
|
771
|
-
] : [
|
|
772
|
-
"\uC774 pull request\uC758 diff\uB97C `gh pr diff`\uC640 `gh pr view`\uB85C \uD655\uC778\uD558\uACE0 \uB9AC\uBDF0\uD558\uC138\uC694.",
|
|
773
|
-
"\uAD6C\uCCB4\uC801 \uC9C0\uC801\uC740 inline comment\uB85C, \uC804\uCCB4 \uC694\uC57D\uC740 \uC815\uD655\uD788 \uD55C \uAC1C\uC758 top-level comment\uB85C \uAC8C\uC2DC\uD558\uC138\uC694.",
|
|
774
|
-
`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.`,
|
|
775
|
-
'\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).',
|
|
776
|
-
"\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).",
|
|
777
|
-
"\uC694\uC57D\uACFC \uC124\uBA85\uC740 \uD55C\uAD6D\uC5B4\uB85C \uC4F0\uB418, \uD30C\uC77C\uBA85\xB7\uD568\uC218\uBA85\xB7\uC2DD\uBCC4\uC790\uB294 \uC6D0\uBB38 \uADF8\uB300\uB85C \uC720\uC9C0\uD558\uC138\uC694."
|
|
778
|
-
];
|
|
779
|
-
return lines.join("\n");
|
|
780
|
-
}
|
|
781
|
-
function composePrompt3(input) {
|
|
782
|
-
const mode = REVIEW_MODES[input.mode];
|
|
783
|
-
const sections = [renderWorkflowInstructions3(input.locale), buildCorrectnessSecurityPrompt(input.locale, mode.promptStyle)];
|
|
784
|
-
if (mode.includesOverEngineering) {
|
|
785
|
-
sections.push(buildOverEngineeringPrompt(input.locale));
|
|
786
|
-
}
|
|
787
|
-
sections.push(renderFindingsBlockInstruction(input.locale));
|
|
788
|
-
return sections.join("\n\n---\n\n");
|
|
789
|
-
}
|
|
790
|
-
var GEMINI_CLI_VERSION = "0.65.0";
|
|
791
|
-
function buildWorkflowYaml3(input) {
|
|
792
|
-
return renderReviewWorkflow(input, {
|
|
793
|
-
name: "Gemini Code Review",
|
|
794
|
-
secretName: SECRET_NAME3,
|
|
795
|
-
// An API key: run-gemini-cli uses OIDC only for Google Cloud workload identity federation.
|
|
796
|
-
idToken: false,
|
|
797
|
-
reviewSteps: [
|
|
798
|
-
renderCheckoutStep(REVIEW_GATE_CONDITION),
|
|
799
|
-
[
|
|
800
|
-
"- name: Run Gemini review",
|
|
801
|
-
` if: ${REVIEW_GATE_CONDITION}`,
|
|
802
|
-
` uses: ${pinnedUses("google-github-actions/run-gemini-cli")}`,
|
|
803
|
-
" env:",
|
|
804
|
-
" GITHUB_TOKEN: ${{ github.token }}",
|
|
805
|
-
// Gemini CLI 0.65 refuses to run in a folder it hasn't been told to trust (measured
|
|
806
|
-
// 2026-10-25 on Actions). The folder is this run's own checkout; pull requests from forks
|
|
807
|
-
// never get here, because they get no secret.
|
|
808
|
-
" GEMINI_CLI_TRUST_WORKSPACE: 'true'",
|
|
809
|
-
" with:",
|
|
810
|
-
` gemini_api_key: \${{ secrets.${SECRET_NAME3} }}`,
|
|
811
|
-
` gemini_cli_version: '${GEMINI_CLI_VERSION}'`,
|
|
812
|
-
" prompt: |",
|
|
813
|
-
indentBlock(composePrompt3(input), 6)
|
|
814
|
-
].join("\n")
|
|
815
|
-
]
|
|
816
|
-
});
|
|
817
|
-
}
|
|
818
|
-
var geminiProvider = {
|
|
819
|
-
id: "gemini",
|
|
820
|
-
workflowFileName: ".github/workflows/gemini-code-review.yml",
|
|
821
|
-
requiredSecretName: SECRET_NAME3,
|
|
822
|
-
buildWorkflowYaml: buildWorkflowYaml3
|
|
823
|
-
};
|
|
824
|
-
|
|
825
|
-
// ../template-engine/dist/review/gpt/gpt-provider.js
|
|
826
|
-
var SECRET_NAME4 = "OPENAI_API_KEY";
|
|
827
|
-
function renderWorkflowInstructions4(locale) {
|
|
828
|
-
const lines = locale === "en" ? [
|
|
829
|
-
"Review ONLY the changes introduced by this pull request. The base and head commits are already fetched under fixed ref names: run `git diff origin/pr-base...origin/pr-head` (and `git log origin/pr-base..origin/pr-head` for commit context) \u2014 this sandboxed step has no network access, so do not attempt to run `gh` or reach the GitHub API.",
|
|
830
|
-
"Write your entire response as a single message: report specific findings, then close with one overall summary.",
|
|
831
|
-
`Always include the literal marker ${ROUND_MARKER} in your message.`,
|
|
832
|
-
"If there are no findings that require action, write exactly one message saying so (and still include the marker).",
|
|
833
|
-
"Do not modify any code, and never approve or merge the pull request.",
|
|
834
|
-
"Write summaries and explanations in English; keep file names, function names, and identifiers verbatim."
|
|
835
|
-
] : [
|
|
836
|
-
"\uC774 pull request\uAC00 \uB3C4\uC785\uD55C \uBCC0\uACBD \uC0AC\uD56D\uB9CC \uB9AC\uBDF0\uD558\uC138\uC694. base/head \uCEE4\uBC0B\uC740 \uACE0\uC815 ref \uC774\uB984\uC73C\uB85C \uC774\uBBF8 fetch\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4: `git diff origin/pr-base...origin/pr-head`\uB85C \uBE44\uAD50\uD558\uACE0, \uCEE4\uBC0B \uB9E5\uB77D\uC740 `git log origin/pr-base..origin/pr-head`\uB85C \uD655\uC778\uD558\uC138\uC694(\uC774 \uC0CC\uB4DC\uBC15\uC2A4 \uC2A4\uD15D\uC740 \uB124\uD2B8\uC6CC\uD06C\uAC00 \uC5C6\uC73C\uBBC0\uB85C `gh`\uB098 GitHub API\uB97C \uD638\uCD9C\uD558\uC9C0 \uB9C8\uC138\uC694).",
|
|
837
|
-
"\uC804\uCCB4 \uC751\uB2F5\uC744 \uD558\uB098\uC758 \uBA54\uC2DC\uC9C0\uB85C \uC791\uC131\uD558\uC138\uC694: \uAD6C\uCCB4\uC801 \uC9C0\uC801\uC744 \uC801\uC740 \uB4A4 \uB9C8\uC9C0\uB9C9\uC5D0 \uC804\uCCB4 \uC694\uC57D\uC744 \uD55C \uBC88 \uB354 \uB0A8\uAE30\uC138\uC694.",
|
|
838
|
-
`\uBA54\uC2DC\uC9C0\uC5D0\uB294 \uBC18\uB4DC\uC2DC \uB9AC\uD130\uB7F4 \uB9C8\uCEE4 ${ROUND_MARKER}\uB97C \uD3EC\uD568\uD558\uC138\uC694.`,
|
|
839
|
-
'\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 \uBA54\uC2DC\uC9C0\uB97C \uC815\uD655\uD788 \uD55C \uAC1C \uC791\uC131\uD558\uC138\uC694(\uC774\uB54C\uB3C4 \uB9C8\uCEE4\uB294 \uD3EC\uD568).',
|
|
840
|
-
"\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).",
|
|
841
|
-
"\uC694\uC57D\uACFC \uC124\uBA85\uC740 \uD55C\uAD6D\uC5B4\uB85C \uC4F0\uB418, \uD30C\uC77C\uBA85\xB7\uD568\uC218\uBA85\xB7\uC2DD\uBCC4\uC790\uB294 \uC6D0\uBB38 \uADF8\uB300\uB85C \uC720\uC9C0\uD558\uC138\uC694."
|
|
842
|
-
];
|
|
843
|
-
return lines.join("\n");
|
|
844
|
-
}
|
|
845
|
-
function composePrompt4(input) {
|
|
846
|
-
const mode = REVIEW_MODES[input.mode];
|
|
847
|
-
const sections = [renderWorkflowInstructions4(input.locale), buildCorrectnessSecurityPrompt(input.locale, mode.promptStyle)];
|
|
848
|
-
if (mode.includesOverEngineering) {
|
|
849
|
-
sections.push(buildOverEngineeringPrompt(input.locale));
|
|
850
|
-
}
|
|
851
|
-
sections.push(renderFindingsBlockInstruction(input.locale));
|
|
852
|
-
return sections.join("\n\n---\n\n");
|
|
853
|
-
}
|
|
854
|
-
function buildWorkflowYaml4(input) {
|
|
855
|
-
return renderReviewWorkflow(input, {
|
|
856
|
-
name: "GPT Code Review",
|
|
857
|
-
secretName: SECRET_NAME4,
|
|
858
|
-
// An API key: codex-action needs no OIDC token.
|
|
859
|
-
idToken: false,
|
|
860
|
-
reviewSteps: [
|
|
861
|
-
renderCheckoutStep(REVIEW_GATE_CONDITION, ["ref: refs/pull/${{ github.event.pull_request.number }}/merge"]),
|
|
862
|
-
[
|
|
863
|
-
"- name: Fetch base and head refs for the PR",
|
|
864
|
-
` if: ${REVIEW_GATE_CONDITION}`,
|
|
865
|
-
" env:",
|
|
866
|
-
" PR_BASE_REF: ${{ github.event.pull_request.base.ref }}",
|
|
867
|
-
" PR_NUMBER: ${{ github.event.pull_request.number }}",
|
|
868
|
-
" run: |",
|
|
869
|
-
" # Explicit destinations: the sandboxed review step has no network, so both sides",
|
|
870
|
-
" # of the diff must survive as stable local refs (FETCH_HEAD alone is ambiguous).",
|
|
871
|
-
" git fetch --no-tags origin \\",
|
|
872
|
-
' "+refs/heads/$PR_BASE_REF:refs/remotes/origin/pr-base" \\',
|
|
873
|
-
' "+refs/pull/$PR_NUMBER/head:refs/remotes/origin/pr-head"'
|
|
874
|
-
].join("\n"),
|
|
875
|
-
[
|
|
876
|
-
"- name: Run Codex review",
|
|
877
|
-
" id: codex-review",
|
|
878
|
-
` if: ${REVIEW_GATE_CONDITION}`,
|
|
879
|
-
` uses: ${pinnedUses("openai/codex-action")}`,
|
|
880
|
-
" with:",
|
|
881
|
-
` openai-api-key: \${{ secrets.${SECRET_NAME4} }}`,
|
|
882
|
-
' permission-profile: ":read-only"',
|
|
883
|
-
" prompt: |",
|
|
884
|
-
indentBlock(composePrompt4(input), 6)
|
|
885
|
-
].join("\n"),
|
|
886
|
-
[
|
|
887
|
-
"- name: Post review comment",
|
|
888
|
-
` if: ${REVIEW_GATE_CONDITION}`,
|
|
889
|
-
" env:",
|
|
890
|
-
" GH_TOKEN: ${{ github.token }}",
|
|
891
|
-
" PR_NUMBER: ${{ github.event.pull_request.number }}",
|
|
892
|
-
" REVIEW_BODY: ${{ steps.codex-review.outputs.final-message }}",
|
|
893
|
-
" run: |",
|
|
894
|
-
' printf %s "$REVIEW_BODY" | gh pr comment "$PR_NUMBER" --body-file -'
|
|
895
|
-
].join("\n")
|
|
896
|
-
]
|
|
897
|
-
});
|
|
898
|
-
}
|
|
899
|
-
var gptProvider = {
|
|
900
|
-
id: "gpt",
|
|
901
|
-
workflowFileName: ".github/workflows/gpt-code-review.yml",
|
|
902
|
-
requiredSecretName: SECRET_NAME4,
|
|
903
|
-
buildWorkflowYaml: buildWorkflowYaml4
|
|
904
|
-
};
|
|
905
|
-
|
|
906
|
-
// ../template-engine/dist/review/provider.js
|
|
907
|
-
var REVIEW_PROVIDERS = {
|
|
908
|
-
claude: claudeProvider,
|
|
909
|
-
gemini: geminiProvider,
|
|
910
|
-
gpt: gptProvider,
|
|
911
|
-
cursor: cursorProvider
|
|
912
|
-
};
|
|
913
|
-
|
|
914
|
-
// ../template-engine/dist/review/compose-review-setup.js
|
|
915
|
-
function composeReviewSetup(input) {
|
|
916
|
-
if (!input.enabled)
|
|
917
|
-
return null;
|
|
918
|
-
const provider = REVIEW_PROVIDERS[input.provider];
|
|
919
|
-
return {
|
|
920
|
-
workflowFilePath: provider.workflowFileName,
|
|
921
|
-
workflowYaml: provider.buildWorkflowYaml(input),
|
|
922
|
-
requiredSecretName: provider.requiredSecretName,
|
|
923
|
-
configSnippet: buildReviewConfigSnippet(input)
|
|
924
|
-
};
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
// ../template-engine/dist/review/conflict.js
|
|
928
|
-
function judgeReviewFileConflict(existing, generated) {
|
|
929
|
-
if (existing === null)
|
|
930
|
-
return { kind: "create" };
|
|
931
|
-
if (existing === generated)
|
|
932
|
-
return { kind: "unchanged" };
|
|
933
|
-
return { kind: "merge-proposal", existing, generated };
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
// ../template-engine/dist/backend/eslint-boundaries.js
|
|
937
|
-
var DEFAULT_BOUNDARY_TAGS = { frontend: "scope:frontend", backend: "scope:backend", shared: "scope:shared" };
|
|
938
|
-
function readBoundaryTags(config) {
|
|
939
|
-
const boundaries = config?.["boundaries"];
|
|
940
|
-
const tags = typeof boundaries === "object" && boundaries !== null ? boundaries["tags"] : void 0;
|
|
941
|
-
const pick = (role) => {
|
|
942
|
-
const value = typeof tags === "object" && tags !== null ? tags[role] : void 0;
|
|
943
|
-
return typeof value === "string" && value.trim() !== "" ? value : DEFAULT_BOUNDARY_TAGS[role];
|
|
944
|
-
};
|
|
945
|
-
return { frontend: pick("frontend"), backend: pick("backend"), shared: pick("shared") };
|
|
946
|
-
}
|
|
947
|
-
function scopeConstraints(tags = DEFAULT_BOUNDARY_TAGS) {
|
|
948
|
-
return [
|
|
949
|
-
{ sourceTag: tags.frontend, notDependOnLibsWithTags: [tags.backend] },
|
|
950
|
-
{ sourceTag: tags.backend, notDependOnLibsWithTags: [tags.frontend] },
|
|
951
|
-
{ sourceTag: tags.shared, notDependOnLibsWithTags: [tags.frontend, tags.backend] }
|
|
952
|
-
];
|
|
953
|
-
}
|
|
954
|
-
var SCOPE_CONSTRAINTS = scopeConstraints();
|
|
955
|
-
var ESLINT_CONFIG_CANDIDATES = ["eslint.config.mjs", "eslint.config.js", "eslint.config.cjs"];
|
|
956
|
-
var appliedMarker = (tags) => `sourceTag: '${tags.frontend}'`;
|
|
957
|
-
var ALLOW_ALL_BLOCK = /depConstraints:\s*\[\s*\{\s*sourceTag:\s*'\*',\s*onlyDependOnLibsWithTags:\s*\[\s*'\*'\s*\],?\s*\},?\s*\]/g;
|
|
958
|
-
function maskComments(text) {
|
|
959
|
-
const out = text.split("");
|
|
960
|
-
let mode = "code";
|
|
961
|
-
let quote = "";
|
|
962
|
-
for (let i = 0; i < text.length; i++) {
|
|
963
|
-
const c = text[i];
|
|
964
|
-
const d = text[i + 1];
|
|
965
|
-
if (mode === "code") {
|
|
966
|
-
if (c === "'" || c === '"' || c === "`") {
|
|
967
|
-
mode = "string";
|
|
968
|
-
quote = c;
|
|
969
|
-
} else if (c === "/" && d === "/") {
|
|
970
|
-
mode = "line";
|
|
971
|
-
out[i] = " ";
|
|
972
|
-
} else if (c === "/" && d === "*") {
|
|
973
|
-
mode = "block";
|
|
974
|
-
out[i] = " ";
|
|
975
|
-
}
|
|
976
|
-
} else if (mode === "string") {
|
|
977
|
-
if (c === "\\") {
|
|
978
|
-
i++;
|
|
979
|
-
} else if (c === quote) {
|
|
980
|
-
mode = "code";
|
|
981
|
-
}
|
|
982
|
-
} else if (mode === "line") {
|
|
983
|
-
if (c === "\n") {
|
|
984
|
-
mode = "code";
|
|
985
|
-
} else {
|
|
986
|
-
out[i] = " ";
|
|
987
|
-
}
|
|
988
|
-
} else {
|
|
989
|
-
if (c === "*" && d === "/") {
|
|
990
|
-
out[i] = " ";
|
|
991
|
-
out[i + 1] = " ";
|
|
992
|
-
i++;
|
|
993
|
-
mode = "code";
|
|
994
|
-
} else if (c !== "\n") {
|
|
995
|
-
out[i] = " ";
|
|
996
|
-
}
|
|
997
|
-
}
|
|
998
|
-
}
|
|
999
|
-
return out.join("");
|
|
1000
|
-
}
|
|
1001
|
-
function findAllowAllMatches(masked) {
|
|
1002
|
-
const matches = [];
|
|
1003
|
-
ALLOW_ALL_BLOCK.lastIndex = 0;
|
|
1004
|
-
for (let m = ALLOW_ALL_BLOCK.exec(masked); m !== null; m = ALLOW_ALL_BLOCK.exec(masked)) {
|
|
1005
|
-
matches.push({ index: m.index, length: m[0].length });
|
|
1006
|
-
}
|
|
1007
|
-
return matches;
|
|
1008
|
-
}
|
|
1009
|
-
function classifyBoundaryConstraints(configText, tags = DEFAULT_BOUNDARY_TAGS) {
|
|
1010
|
-
const masked = maskComments(configText);
|
|
1011
|
-
if (masked.includes(appliedMarker(tags)))
|
|
1012
|
-
return "enforced";
|
|
1013
|
-
if (findAllowAllMatches(masked).length > 0)
|
|
1014
|
-
return "allow-all";
|
|
1015
|
-
return "unknown";
|
|
1016
|
-
}
|
|
1017
|
-
function renderConstraintsBlock(indent, eol, tags) {
|
|
1018
|
-
const renderList = (list) => list.map((t) => `'${t}'`).join(", ");
|
|
1019
|
-
const inner = scopeConstraints(tags).map((c) => {
|
|
1020
|
-
const field = c.notDependOnLibsWithTags ? `notDependOnLibsWithTags: [${renderList(c.notDependOnLibsWithTags)}]` : `onlyDependOnLibsWithTags: [${renderList(c.onlyDependOnLibsWithTags ?? [])}]`;
|
|
1021
|
-
return [`${indent} {`, `${indent} sourceTag: '${c.sourceTag}',`, `${indent} ${field},`, `${indent} },`].join(eol);
|
|
1022
|
-
}).join(eol);
|
|
1023
|
-
return [
|
|
1024
|
-
`// nx-archweave boundaries \u2014 the frontend and the backend may never import each other`,
|
|
1025
|
-
`${indent}depConstraints: [`,
|
|
1026
|
-
inner,
|
|
1027
|
-
[
|
|
1028
|
-
`${indent} // baseline: everything else stays allowed (constraints apply conjunctively)`,
|
|
1029
|
-
`${indent} {`,
|
|
1030
|
-
`${indent} sourceTag: '*',`,
|
|
1031
|
-
`${indent} onlyDependOnLibsWithTags: ['*'],`,
|
|
1032
|
-
`${indent} },`
|
|
1033
|
-
].join(eol),
|
|
1034
|
-
`${indent}]`
|
|
1035
|
-
].join(eol);
|
|
1036
|
-
}
|
|
1037
|
-
function applyBoundaryConstraints(configText, tags = DEFAULT_BOUNDARY_TAGS) {
|
|
1038
|
-
const classification = classifyBoundaryConstraints(configText, tags);
|
|
1039
|
-
if (classification === "enforced")
|
|
1040
|
-
return { status: "already-applied" };
|
|
1041
|
-
if (classification === "unknown")
|
|
1042
|
-
return { status: "unrecognized" };
|
|
1043
|
-
const masked = maskComments(configText);
|
|
1044
|
-
const matches = findAllowAllMatches(masked);
|
|
1045
|
-
if (matches.length !== 1) {
|
|
1046
|
-
return { status: "unrecognized" };
|
|
1047
|
-
}
|
|
1048
|
-
const [match] = matches;
|
|
1049
|
-
const eol = configText.includes("\r\n") ? "\r\n" : "\n";
|
|
1050
|
-
const lineStart = configText.lastIndexOf("\n", match.index) + 1;
|
|
1051
|
-
const indent = /^[ \t]*/.exec(configText.slice(lineStart, match.index))?.[0] ?? "";
|
|
1052
|
-
const replacement = renderConstraintsBlock(indent, eol, tags);
|
|
1053
|
-
const text = configText.slice(0, match.index) + replacement + configText.slice(match.index + match.length);
|
|
1054
|
-
return { status: "applied", text };
|
|
1055
|
-
}
|
|
1056
|
-
function buildBoundarySnippet(tags = DEFAULT_BOUNDARY_TAGS) {
|
|
1057
|
-
return [
|
|
1058
|
-
"// nx-archweave project boundaries \u2014 merge these entries into the depConstraints",
|
|
1059
|
-
"// array of the '@nx/enforce-module-boundaries' rule in your root ESLint config.",
|
|
1060
|
-
"// They ride on top of your existing entries: constraints apply conjunctively,",
|
|
1061
|
-
"// so the allow-all baseline can stay.",
|
|
1062
|
-
renderConstraintsBlock("", "\n", tags)
|
|
1063
|
-
].join("\n");
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
// ../templates/dist/agents/agent-sections.js
|
|
1067
|
-
var UNIVERSAL_AGENT_SECTION_IDS = [
|
|
1068
|
-
"think-before-coding",
|
|
1069
|
-
"simplicity-first",
|
|
1070
|
-
"surgical-changes",
|
|
1071
|
-
"goal-driven-execution",
|
|
1072
|
-
"code-structure"
|
|
1073
|
-
];
|
|
1074
|
-
var PLACEHOLDER_AGENT_SECTION_IDS = ["project-goal", "architecture-boundary"];
|
|
1075
|
-
var TEAM_AGENT_SECTION_ID = "team-rules";
|
|
1076
|
-
var FSD_AGENT_SECTION_ID = "fsd-structure";
|
|
1077
|
-
var DEFAULT_AGENT_SECTION_IDS = [
|
|
1078
|
-
...UNIVERSAL_AGENT_SECTION_IDS,
|
|
1079
|
-
"safety-rules",
|
|
1080
|
-
"language-policy",
|
|
1081
|
-
...PLACEHOLDER_AGENT_SECTION_IDS
|
|
1082
|
-
];
|
|
1083
|
-
var EN_SECTIONS = {
|
|
1084
|
-
"think-before-coding": {
|
|
1085
|
-
id: "think-before-coding",
|
|
1086
|
-
heading: "Think Before Coding",
|
|
1087
|
-
body: [
|
|
1088
|
-
"Before writing any code, restate the problem in your own words and identify what could go wrong.",
|
|
1089
|
-
"Read the surrounding code and existing patterns before introducing new ones \u2014 consistency beats",
|
|
1090
|
-
"personal preference. When a request is ambiguous, make the most reasonable interpretation and say",
|
|
1091
|
-
"what you assumed, rather than guessing silently or blocking on a question you could answer yourself.",
|
|
1092
|
-
"Prefer understanding the root cause of a problem over patching its symptoms."
|
|
1093
|
-
].join("\n")
|
|
1094
|
-
},
|
|
1095
|
-
"simplicity-first": {
|
|
1096
|
-
id: "simplicity-first",
|
|
1097
|
-
heading: "Simplicity First",
|
|
1098
|
-
body: [
|
|
1099
|
-
"Choose the simplest design that solves the actual problem in front of you, not the one you might",
|
|
1100
|
-
"have tomorrow. Avoid speculative abstraction, unnecessary configuration, and generic frameworks",
|
|
1101
|
-
"built for requirements nobody has asked for yet. A straightforward, slightly duplicated solution",
|
|
1102
|
-
"that is easy to read beats a clever, general one that is hard to follow. Add complexity only when",
|
|
1103
|
-
"a concrete, current need demands it."
|
|
1104
|
-
].join("\n")
|
|
1105
|
-
},
|
|
1106
|
-
"surgical-changes": {
|
|
1107
|
-
id: "surgical-changes",
|
|
1108
|
-
heading: "Surgical Changes",
|
|
1109
|
-
body: [
|
|
1110
|
-
"Change only what the task requires. Do not reformat, rename, or refactor unrelated code while",
|
|
1111
|
-
"fixing something else \u2014 unrelated changes make review harder and hide the actual fix in noise.",
|
|
1112
|
-
"Keep diffs small and focused so a reviewer can see exactly what changed and why. If you notice an",
|
|
1113
|
-
"unrelated problem while working, note it separately instead of folding it into the current change."
|
|
1114
|
-
].join("\n")
|
|
1115
|
-
},
|
|
1116
|
-
"goal-driven-execution": {
|
|
1117
|
-
id: "goal-driven-execution",
|
|
1118
|
-
heading: "Goal-Driven Execution",
|
|
1119
|
-
body: [
|
|
1120
|
-
"Keep the actual goal in view, not just the literal instruction. If a requested step turns out to",
|
|
1121
|
-
"be unnecessary, harmful, or based on a wrong assumption once you're in the code, say so and propose",
|
|
1122
|
-
"the better path instead of executing it mechanically. Finish what you start: a task is done when it",
|
|
1123
|
-
"actually works, not when the obvious-looking code has been written. Verify before declaring success."
|
|
1124
|
-
].join("\n")
|
|
1125
|
-
},
|
|
1126
|
-
"code-structure": {
|
|
1127
|
-
id: "code-structure",
|
|
1128
|
-
heading: "Code Structure and Abstraction",
|
|
1129
|
-
body: [
|
|
1130
|
-
"Keep functions and modules focused on one responsibility; split a file or function when it starts",
|
|
1131
|
-
"accumulating unrelated concerns rather than letting it grow into a god object. Name things after",
|
|
1132
|
-
"what they do or represent, not after their implementation detail. Extract an abstraction only after",
|
|
1133
|
-
"a pattern has actually repeated \u2014 not in anticipation of a repetition that may never come."
|
|
1134
|
-
].join("\n")
|
|
1135
|
-
},
|
|
1136
|
-
"safety-rules": {
|
|
1137
|
-
id: "safety-rules",
|
|
1138
|
-
heading: "Safety Rules",
|
|
1139
|
-
body: [
|
|
1140
|
-
"Never force-push to shared branches, never bypass required status checks, and never disable or",
|
|
1141
|
-
"delete CI/CD configuration just to make a failing check pass.",
|
|
1142
|
-
"Do not read, print, or commit the contents of secret files (`.env`, credential files, private",
|
|
1143
|
-
"keys) \u2014 treat anything matching a common secret pattern as off-limits unless the user has already",
|
|
1144
|
-
"shared its contents with you directly.",
|
|
1145
|
-
"Do not run destructive commands (`rm -rf`, `git reset --hard`, `git clean -f`, force pushes)",
|
|
1146
|
-
"without explicit confirmation, and check for uncommitted work first.",
|
|
1147
|
-
"Do not send repository contents, credentials, or user data to any external service.",
|
|
1148
|
-
"When you are unsure whether an action is safe, stop and ask instead of proceeding.",
|
|
1149
|
-
"",
|
|
1150
|
-
"<!-- TODO: list project-specific protected resources (e.g. production config files, protected",
|
|
1151
|
-
"branches, restricted directories, external services this project must never call in tests). -->"
|
|
1152
|
-
].join("\n")
|
|
1153
|
-
},
|
|
1154
|
-
"language-policy": {
|
|
1155
|
-
id: "language-policy",
|
|
1156
|
-
heading: "Language Policy",
|
|
1157
|
-
body: [
|
|
1158
|
-
"Communicate with the user in English. Write code, identifiers, comments, commit messages, and",
|
|
1159
|
-
"documentation in English as well, unless the user explicitly asks for another language for a",
|
|
1160
|
-
"specific piece of content."
|
|
1161
|
-
].join("\n")
|
|
1162
|
-
},
|
|
1163
|
-
"team-rules": {
|
|
1164
|
-
id: "team-rules",
|
|
1165
|
-
heading: "Team Rules",
|
|
1166
|
-
body: [
|
|
1167
|
-
"This repository is shared by a team: optimize for the reviewer, not for yourself.",
|
|
1168
|
-
"Keep each pull request small and about one thing, fill in the pull request template, and link",
|
|
1169
|
-
"the issue it closes.",
|
|
1170
|
-
"Treat review findings as work, not opinions: address every P0/P1 finding before asking for",
|
|
1171
|
-
"another look, answer each comment, and never merge your own pull request or dismiss a review",
|
|
1172
|
-
"to get it through.",
|
|
1173
|
-
"Respect module ownership: a change that crosses a module or layer boundary needs that",
|
|
1174
|
-
"module's owner in the review. Never weaken a module-boundary lint rule, add an eslint-disable",
|
|
1175
|
-
"for one, or retag a project to get an import past the boundary \u2014 propose the boundary change",
|
|
1176
|
-
"in the pull request description instead.",
|
|
1177
|
-
"Changes to shared code (the `shared` layer, `shared-types`, root configuration) affect every",
|
|
1178
|
-
"team member: keep them backward compatible, call them out in the pull request, and keep them",
|
|
1179
|
-
"separate from feature work.",
|
|
1180
|
-
"Never commit directly to a protected branch; work on a branch and bring it up to date with",
|
|
1181
|
-
"its base before requesting review.",
|
|
1182
|
-
"",
|
|
1183
|
-
"<!-- TODO: list team-specific conventions (module owners / CODEOWNERS, required reviewers,",
|
|
1184
|
-
"branch naming, release process). -->"
|
|
1185
|
-
].join("\n")
|
|
1186
|
-
},
|
|
1187
|
-
"fsd-structure": {
|
|
1188
|
-
id: "fsd-structure",
|
|
1189
|
-
heading: "Frontend Structure (FSD Lite)",
|
|
1190
|
-
body: [
|
|
1191
|
-
'The frontend app follows canonical Feature-Sliced Design. "Lite" means only that the default scope',
|
|
1192
|
-
"is `app`, `pages`, `features`, `entities` and `shared` (`widgets` only on request) and that Solo",
|
|
1193
|
-
"workspaces get warnings instead of errors \u2014 the rules themselves are the canonical ones:",
|
|
1194
|
-
"- A layer imports only layers below it: app > pages > widgets > features > entities > shared.",
|
|
1195
|
-
"- Slices on one layer (pages, widgets, features, entities) never import each other; move the",
|
|
1196
|
-
" shared part one layer down, or expose it through the other slice's `@x/<consumer>` public API.",
|
|
1197
|
-
"- Import a slice only through its `index.ts`; never reach into its `ui/`, `model/` or `api/` files.",
|
|
1198
|
-
"- `app` and `shared` hold segments, not slices; shared segments are named by purpose (`ui`, `api`,",
|
|
1199
|
-
" `model`, `lib`, `config`), never by essence (`components`, `hooks`, `utils`, `types`).",
|
|
1200
|
-
"Create slices with the generators (`nx g @clearlydev/nx-archweave:page|widget|feature|entity|shared --name=\u2026`,",
|
|
1201
|
-
"`--segments=ui,api` for extra segments).",
|
|
1202
|
-
"The `archweave/fsd-boundaries` ESLint rule reports violations: fix the import, never disable the",
|
|
1203
|
-
"rule or add an eslint-disable for it. On Next.js the pages layer is the `_pages/` directory."
|
|
1204
|
-
].join("\n")
|
|
1205
|
-
},
|
|
1206
|
-
"project-goal": {
|
|
1207
|
-
id: "project-goal",
|
|
1208
|
-
heading: "Project Goal",
|
|
1209
|
-
body: "<!-- TODO: describe this project's goal, target users, and what success looks like. -->"
|
|
1210
|
-
},
|
|
1211
|
-
"architecture-boundary": {
|
|
1212
|
-
id: "architecture-boundary",
|
|
1213
|
-
heading: "Architecture Boundary",
|
|
1214
|
-
body: "<!-- TODO: describe this project's module boundaries, ownership rules, and which modules may or may not depend on which. -->"
|
|
1215
|
-
}
|
|
1216
|
-
};
|
|
1217
|
-
var KO_SECTIONS = {
|
|
1218
|
-
"think-before-coding": {
|
|
1219
|
-
id: "think-before-coding",
|
|
1220
|
-
heading: "\uCF54\uB529 \uC804\uC5D0 \uBA3C\uC800 \uC0DD\uAC01\uD558\uAE30",
|
|
1221
|
-
body: [
|
|
1222
|
-
"\uCF54\uB4DC\uB97C \uC791\uC131\uD558\uAE30 \uC804\uC5D0 \uBB38\uC81C\uB97C \uC2A4\uC2A4\uB85C\uC758 \uB9D0\uB85C \uB2E4\uC2DC \uC815\uB9AC\uD558\uACE0 \uBB34\uC5C7\uC774 \uC798\uBABB\uB420 \uC218 \uC788\uB294\uC9C0 \uD30C\uC545\uD55C\uB2E4.",
|
|
1223
|
-
"\uC0C8 \uD328\uD134\uC744 \uB3C4\uC785\uD558\uAE30 \uC804\uC5D0 \uC8FC\uBCC0 \uCF54\uB4DC\uC640 \uAE30\uC874 \uAD00\uB840\uB97C \uBA3C\uC800 \uC77D\uB294\uB2E4 \u2014 \uAC1C\uC778 \uCDE8\uD5A5\uBCF4\uB2E4 \uC77C\uAD00\uC131\uC774 \uC6B0\uC120\uC774\uB2E4.",
|
|
1224
|
-
"\uC694\uCCAD\uC774 \uBAA8\uD638\uD560 \uB54C\uB294 \uAC00\uC7A5 \uD569\uB9AC\uC801\uC778 \uD574\uC11D\uC744 \uD0DD\uD558\uACE0 \uBB34\uC5C7\uC744 \uAC00\uC815\uD588\uB294\uC9C0 \uBC1D\uD78C\uB2E4. \uC2A4\uC2A4\uB85C \uB2F5\uD560 \uC218 \uC788\uB294",
|
|
1225
|
-
"\uC9C8\uBB38\uC73C\uB85C \uC791\uC5C5\uC744 \uB9C9\uC9C0 \uB9D0\uACE0, \uC870\uC6A9\uD788 \uCD94\uCE21\uB9CC \uD558\uC9C0\uB3C4 \uC54A\uB294\uB2E4. \uC99D\uC0C1\uC744 \uB55C\uC9C8\uD558\uAE30\uBCF4\uB2E4 \uADFC\uBCF8 \uC6D0\uC778\uC744 \uC774\uD574\uD558\uB294",
|
|
1226
|
-
"\uCABD\uC744 \uC6B0\uC120\uD55C\uB2E4."
|
|
1227
|
-
].join("\n")
|
|
1228
|
-
},
|
|
1229
|
-
"simplicity-first": {
|
|
1230
|
-
id: "simplicity-first",
|
|
1231
|
-
heading: "\uB2E8\uC21C\uD568 \uC6B0\uC120",
|
|
1232
|
-
body: [
|
|
1233
|
-
"\uC9C0\uAE08 \uB208\uC55E\uC758 \uBB38\uC81C\uB97C \uD574\uACB0\uD558\uB294 \uAC00\uC7A5 \uB2E8\uC21C\uD55C \uC124\uACC4\uB97C \uC120\uD0DD\uD55C\uB2E4 \u2014 \uB0B4\uC77C \uD544\uC694\uD560\uC9C0\uB3C4 \uBAA8\uB974\uB294 \uAC83\uAE4C\uC9C0 \uB300\uBE44\uD558\uC9C0",
|
|
1234
|
-
"\uC54A\uB294\uB2E4. \uC544\uBB34\uB3C4 \uC694\uCCAD\uD558\uC9C0 \uC54A\uC740 \uC694\uAD6C\uC0AC\uD56D\uC744 \uC704\uD55C \uCD94\uCE21\uC131 \uCD94\uC0C1\uD654, \uBD88\uD544\uC694\uD55C \uC124\uC815, \uBC94\uC6A9 \uD504\uB808\uC784\uC6CC\uD06C\uB97C",
|
|
1235
|
-
"\uD53C\uD55C\uB2E4. \uC77D\uAE30 \uC26C\uC6B4 \uC57D\uAC04\uC758 \uC911\uBCF5\uC774 \uC788\uB294 \uD574\uBC95\uC774, \uC774\uD574\uD558\uAE30 \uC5B4\uB824\uC6B4 \uC601\uB9AC\uD558\uACE0 \uBC94\uC6A9\uC801\uC778 \uD574\uBC95\uBCF4\uB2E4 \uB0AB\uB2E4.",
|
|
1236
|
-
"\uAD6C\uCCB4\uC801\uC774\uACE0 \uD604\uC7AC \uC874\uC7AC\uD558\uB294 \uD544\uC694\uAC00 \uC788\uC744 \uB54C\uB9CC \uBCF5\uC7A1\uB3C4\uB97C \uCD94\uAC00\uD55C\uB2E4."
|
|
1237
|
-
].join("\n")
|
|
1238
|
-
},
|
|
1239
|
-
"surgical-changes": {
|
|
1240
|
-
id: "surgical-changes",
|
|
1241
|
-
heading: "\uC678\uACFC\uC801 \uBCC0\uACBD",
|
|
1242
|
-
body: [
|
|
1243
|
-
"\uC791\uC5C5\uC5D0 \uD544\uC694\uD55C \uBD80\uBD84\uB9CC \uBCC0\uACBD\uD55C\uB2E4. \uB2E4\uB978 \uBB38\uC81C\uB97C \uACE0\uCE58\uB294 \uAE40\uC5D0 \uAD00\uB828 \uC5C6\uB294 \uCF54\uB4DC\uB97C \uC7AC\uD3EC\uB9F7\uD558\uAC70\uB098, \uC774\uB984\uC744",
|
|
1244
|
-
"\uBC14\uAFB8\uAC70\uB098, \uB9AC\uD329\uD130\uB9C1\uD558\uC9C0 \uC54A\uB294\uB2E4 \u2014 \uAD00\uB828 \uC5C6\uB294 \uBCC0\uACBD\uC740 \uB9AC\uBDF0\uB97C \uC5B4\uB835\uAC8C \uB9CC\uB4E4\uACE0 \uC2E4\uC81C \uC218\uC815 \uB0B4\uC6A9\uC744 \uB178\uC774\uC988",
|
|
1245
|
-
"\uC18D\uC5D0 \uC228\uAE34\uB2E4. diff\uB97C \uC791\uACE0 \uC9D1\uC911\uB418\uAC8C \uC720\uC9C0\uD574 \uB9AC\uBDF0\uC5B4\uAC00 \uBB34\uC5C7\uC774, \uC65C \uBC14\uB00C\uC5C8\uB294\uC9C0 \uC815\uD655\uD788 \uBCFC \uC218 \uC788\uAC8C \uD55C\uB2E4.",
|
|
1246
|
-
"\uC791\uC5C5 \uC911 \uAD00\uB828 \uC5C6\uB294 \uBB38\uC81C\uB97C \uBC1C\uACAC\uD558\uBA74 \uD604\uC7AC \uBCC0\uACBD\uC5D0 \uC11E\uC9C0 \uB9D0\uACE0 \uBCC4\uB3C4\uB85C \uC54C\uB9B0\uB2E4."
|
|
1247
|
-
].join("\n")
|
|
1248
|
-
},
|
|
1249
|
-
"goal-driven-execution": {
|
|
1250
|
-
id: "goal-driven-execution",
|
|
1251
|
-
heading: "\uBAA9\uD45C \uC9C0\uD5A5 \uC2E4\uD589",
|
|
1252
|
-
body: [
|
|
1253
|
-
"\uBB38\uC790 \uADF8\uB300\uB85C\uC758 \uC9C0\uC2DC\uAC00 \uC544\uB2C8\uB77C \uC2E4\uC81C \uBAA9\uD45C\uB97C \uACC4\uC18D \uC5FC\uB450\uC5D0 \uB454\uB2E4. \uCF54\uB4DC\uB97C \uC0B4\uD3B4\uBCF4\uB2C8 \uC694\uCCAD\uBC1B\uC740 \uB2E8\uACC4\uAC00",
|
|
1254
|
-
"\uBD88\uD544\uC694\uD558\uAC70\uB098, \uD574\uB86D\uAC70\uB098, \uC798\uBABB\uB41C \uAC00\uC815\uC5D0 \uAE30\uBC18\uD55C \uAC83\uC73C\uB85C \uB4DC\uB7EC\uB098\uBA74 \uAE30\uACC4\uC801\uC73C\uB85C \uC2E4\uD589\uD558\uC9C0 \uB9D0\uACE0 \uADF8\uB807\uB2E4\uACE0",
|
|
1255
|
-
"\uB9D0\uD55C \uB4A4 \uB354 \uB098\uC740 \uACBD\uB85C\uB97C \uC81C\uC548\uD55C\uB2E4. \uC2DC\uC791\uD55C \uC77C\uC740 \uB05D\uB0B8\uB2E4 \u2014 \uADF8\uB7F4\uB4EF\uD574 \uBCF4\uC774\uB294 \uCF54\uB4DC\uB97C \uC791\uC131\uD588\uC744 \uB54C\uAC00 \uC544\uB2C8\uB77C",
|
|
1256
|
-
"\uC2E4\uC81C\uB85C \uB3D9\uC791\uD560 \uB54C\uAC00 \uC644\uB8CC\uB2E4. \uC131\uACF5\uC744 \uC120\uC5B8\uD558\uAE30 \uC804\uC5D0 \uBC18\uB4DC\uC2DC \uAC80\uC99D\uD55C\uB2E4."
|
|
1257
|
-
].join("\n")
|
|
1258
|
-
},
|
|
1259
|
-
"code-structure": {
|
|
1260
|
-
id: "code-structure",
|
|
1261
|
-
heading: "\uCF54\uB4DC \uAD6C\uC870\uC640 \uCD94\uC0C1\uD654",
|
|
1262
|
-
body: [
|
|
1263
|
-
"\uD568\uC218\uC640 \uBAA8\uB4C8\uC744 \uD558\uB098\uC758 \uCC45\uC784\uC5D0 \uC9D1\uC911\uC2DC\uD0A8\uB2E4. God object\uB85C \uCEE4\uC9C0\uB3C4\uB85D \uB450\uC9C0 \uB9D0\uACE0, \uAD00\uB828 \uC5C6\uB294 \uAD00\uC2EC\uC0AC\uAC00",
|
|
1264
|
-
"\uC313\uC774\uAE30 \uC2DC\uC791\uD558\uBA74 \uD30C\uC77C\uC774\uB098 \uD568\uC218\uB97C \uBD84\uB9AC\uD55C\uB2E4. \uAD6C\uD604 \uC138\uBD80\uC0AC\uD56D\uC774 \uC544\uB2C8\uB77C \uD558\uB294 \uC77C\uC774\uB098 \uB098\uD0C0\uB0B4\uB294 \uB300\uC0C1\uC744",
|
|
1265
|
-
"\uAE30\uC900\uC73C\uB85C \uC774\uB984\uC744 \uC9D3\uB294\uB2E4. \uD328\uD134\uC774 \uC2E4\uC81C\uB85C \uBC18\uBCF5\uB41C \uB4A4\uC5D0\uB9CC \uCD94\uC0C1\uD654\uB97C \uCD94\uCD9C\uD55C\uB2E4 \u2014 \uC5B8\uC820\uAC00 \uBC18\uBCF5\uB420\uC9C0\uB3C4 \uBAA8\uB978\uB2E4\uB294",
|
|
1266
|
-
"\uC608\uCE21\uB9CC\uC73C\uB85C \uBBF8\uB9AC \uCD94\uCD9C\uD558\uC9C0 \uC54A\uB294\uB2E4."
|
|
1267
|
-
].join("\n")
|
|
1268
|
-
},
|
|
1269
|
-
"safety-rules": {
|
|
1270
|
-
id: "safety-rules",
|
|
1271
|
-
heading: "\uC548\uC804 \uADDC\uCE59",
|
|
1272
|
-
body: [
|
|
1273
|
-
"\uACF5\uC720 \uBE0C\uB79C\uCE58\uC5D0 force-push\uD558\uC9C0 \uC54A\uACE0, \uD544\uC218 \uC0C1\uD0DC \uAC80\uC0AC\uB97C \uC6B0\uD68C\uD558\uC9C0 \uC54A\uC73C\uBA70, \uC2E4\uD328\uD558\uB294 \uAC80\uC0AC\uB97C \uD1B5\uACFC\uC2DC\uD0A4\uAE30",
|
|
1274
|
-
"\uC704\uD574 CI/CD \uC124\uC815\uC744 \uBE44\uD65C\uC131\uD654\uD558\uAC70\uB098 \uC0AD\uC81C\uD558\uC9C0 \uC54A\uB294\uB2E4.",
|
|
1275
|
-
"\uBE44\uBC00 \uD30C\uC77C(`.env`, \uC790\uACA9 \uC99D\uBA85 \uD30C\uC77C, \uAC1C\uC778 \uD0A4)\uC758 \uB0B4\uC6A9\uC744 \uC77D\uAC70\uB098 \uCD9C\uB825\uD558\uAC70\uB098 \uCEE4\uBC0B\uD558\uC9C0 \uC54A\uB294\uB2E4 \u2014 \uC0AC\uC6A9\uC790\uAC00",
|
|
1276
|
-
"\uBA3C\uC800 \uC9C1\uC811 \uB0B4\uC6A9\uC744 \uACF5\uC720\uD558\uC9C0 \uC54A\uB294 \uD55C, \uC77C\uBC18\uC801\uC778 \uBE44\uBC00 \uD30C\uC77C \uD328\uD134\uC5D0 \uD574\uB2F9\uD558\uB294 \uAC83\uC740 \uC811\uADFC \uAE08\uC9C0 \uB300\uC0C1\uC73C\uB85C",
|
|
1277
|
-
"\uCDE8\uAE09\uD55C\uB2E4.",
|
|
1278
|
-
"\uBA85\uC2DC\uC801 \uD655\uC778 \uC5C6\uC774 \uD30C\uAD34\uC801 \uBA85\uB839(`rm -rf`, `git reset --hard`, `git clean -f`, force-push)\uC744 \uC2E4\uD589\uD558\uC9C0",
|
|
1279
|
-
"\uC54A\uC73C\uBA70, \uC2E4\uD589 \uC804 \uCEE4\uBC0B\uB418\uC9C0 \uC54A\uC740 \uC791\uC5C5\uC774 \uC788\uB294\uC9C0 \uBA3C\uC800 \uD655\uC778\uD55C\uB2E4.",
|
|
1280
|
-
"\uC800\uC7A5\uC18C \uB0B4\uC6A9, \uC790\uACA9 \uC99D\uBA85, \uC0AC\uC6A9\uC790 \uB370\uC774\uD130\uB97C \uC678\uBD80 \uC11C\uBE44\uC2A4\uB85C \uC804\uC1A1\uD558\uC9C0 \uC54A\uB294\uB2E4.",
|
|
1281
|
-
"\uC5B4\uB5A4 \uB3D9\uC791\uC774 \uC548\uC804\uD55C\uC9C0 \uD655\uC2E0\uC774 \uC11C\uC9C0 \uC54A\uC73C\uBA74 \uC9C4\uD589\uD558\uC9C0 \uB9D0\uACE0 \uBA3C\uC800 \uC9C8\uBB38\uD55C\uB2E4.",
|
|
1282
|
-
"",
|
|
1283
|
-
// The marker itself stays in English, matching the Language Policy's "code/comments in
|
|
1284
|
-
// English" rule and giving tooling a single locale-invariant string to check for.
|
|
1285
|
-
"<!-- TODO: list project-specific protected resources (e.g. production config files, protected",
|
|
1286
|
-
"branches, restricted directories, external services this project must never call in tests). -->"
|
|
1287
|
-
].join("\n")
|
|
1288
|
-
},
|
|
1289
|
-
"language-policy": {
|
|
1290
|
-
id: "language-policy",
|
|
1291
|
-
heading: "\uC5B8\uC5B4 \uC815\uCC45",
|
|
1292
|
-
body: [
|
|
1293
|
-
"\uC0AC\uC6A9\uC790\uC640\uC758 \uB300\uD654\uB294 \uD55C\uAD6D\uC5B4\uB85C \uC9C4\uD589\uD55C\uB2E4. \uCF54\uB4DC, \uC2DD\uBCC4\uC790, \uC8FC\uC11D, \uCEE4\uBC0B \uBA54\uC2DC\uC9C0, \uBB38\uC11C\uB294 \uC0AC\uC6A9\uC790\uAC00 \uD2B9\uC815",
|
|
1294
|
-
"\uD56D\uBAA9\uC5D0 \uB300\uD574 \uB2E4\uB978 \uC5B8\uC5B4\uB97C \uBA85\uC2DC\uC801\uC73C\uB85C \uC694\uCCAD\uD558\uC9C0 \uC54A\uB294 \uD55C \uC601\uC5B4\uB85C \uC791\uC131\uD55C\uB2E4."
|
|
1295
|
-
].join("\n")
|
|
1296
|
-
},
|
|
1297
|
-
"team-rules": {
|
|
1298
|
-
id: "team-rules",
|
|
1299
|
-
heading: "\uD300 \uADDC\uCE59",
|
|
1300
|
-
body: [
|
|
1301
|
-
"\uC774 \uC800\uC7A5\uC18C\uB294 \uD300\uC774 \uD568\uAED8 \uC4F4\uB2E4 \u2014 \uC790\uC2E0\uC774 \uC544\uB2C8\uB77C \uB9AC\uBDF0\uC5B4\uB97C \uC704\uD574 \uCD5C\uC801\uD654\uD55C\uB2E4.",
|
|
1302
|
-
"PR\uC740 \uC791\uAC8C, \uD55C \uAC00\uC9C0 \uC8FC\uC81C\uB9CC \uB2E4\uB8E8\uBA70, PR \uD15C\uD50C\uB9BF\uC744 \uCC44\uC6B0\uACE0 \uB2EB\uB294 \uC774\uC288\uB97C \uB9C1\uD06C\uD55C\uB2E4.",
|
|
1303
|
-
"\uB9AC\uBDF0 finding\uC740 \uC758\uACAC\uC774 \uC544\uB2C8\uB77C \uC791\uC5C5\uC774\uB2E4 \u2014 \uC7AC\uB9AC\uBDF0\uB97C \uC694\uCCAD\uD558\uAE30 \uC804\uC5D0 P0/P1 finding\uC744 \uBAA8\uB450 \uD574\uACB0\uD558\uACE0,",
|
|
1304
|
-
"\uCF54\uBA58\uD2B8\uB9C8\uB2E4 \uB2F5\uD558\uBA70, \uC790\uAE30 PR\uC744 \uC2A4\uC2A4\uB85C \uBA38\uC9C0\uD558\uAC70\uB098 \uB9AC\uBDF0\uB97C dismiss\uD574\uC11C \uD1B5\uACFC\uC2DC\uD0A4\uC9C0 \uC54A\uB294\uB2E4.",
|
|
1305
|
-
"\uBAA8\uB4C8 \uC18C\uC720\uAD8C\uC744 \uC874\uC911\uD55C\uB2E4 \u2014 \uBAA8\uB4C8\uC774\uB098 \uB808\uC774\uC5B4 \uACBD\uACC4\uB97C \uB118\uB294 \uBCC0\uACBD\uC740 \uD574\uB2F9 \uBAA8\uB4C8 \uC624\uB108\uC758 \uB9AC\uBDF0\uAC00 \uD544\uC694\uD558\uB2E4.",
|
|
1306
|
-
"import\uB97C \uACBD\uACC4 \uB108\uBA38\uB85C \uD1B5\uACFC\uC2DC\uD0A4\uAE30 \uC704\uD574 module-boundary \uB9B0\uD2B8 \uADDC\uCE59\uC744 \uC57D\uD654\uD558\uAC70\uB098, eslint-disable\uC744",
|
|
1307
|
-
"\uCD94\uAC00\uD558\uAC70\uB098, \uD504\uB85C\uC81D\uD2B8 \uD0DC\uADF8\uB97C \uBC14\uAFB8\uC9C0 \uC54A\uB294\uB2E4 \u2014 \uACBD\uACC4 \uBCC0\uACBD\uC740 PR \uBCF8\uBB38\uC5D0\uC11C \uC81C\uC548\uD55C\uB2E4.",
|
|
1308
|
-
"\uACF5\uC720 \uCF54\uB4DC(`shared` \uB808\uC774\uC5B4, `shared-types`, \uB8E8\uD2B8 \uC124\uC815) \uBCC0\uACBD\uC740 \uD300 \uC804\uC6D0\uC5D0\uAC8C \uC601\uD5A5\uC744 \uC900\uB2E4 \u2014 \uD558\uC704",
|
|
1309
|
-
"\uD638\uD658\uC744 \uC720\uC9C0\uD558\uACE0, PR\uC5D0 \uBA85\uC2DC\uD558\uBA70, \uAE30\uB2A5 \uC791\uC5C5\uACFC \uBD84\uB9AC\uD55C\uB2E4.",
|
|
1310
|
-
"\uBCF4\uD638 \uBE0C\uB79C\uCE58\uC5D0 \uC9C1\uC811 \uCEE4\uBC0B\uD558\uC9C0 \uC54A\uB294\uB2E4 \u2014 \uBE0C\uB79C\uCE58\uC5D0\uC11C \uC791\uC5C5\uD558\uACE0 \uB9AC\uBDF0 \uC694\uCCAD \uC804\uC5D0 \uBCA0\uC774\uC2A4\uC640 \uB3D9\uAE30\uD654\uD55C\uB2E4.",
|
|
1311
|
-
"",
|
|
1312
|
-
// The marker stays in English, matching the Safety Rules precedent (one locale-invariant
|
|
1313
|
-
// string for tooling to check).
|
|
1314
|
-
"<!-- TODO: list team-specific conventions (module owners / CODEOWNERS, required reviewers,",
|
|
1315
|
-
"branch naming, release process). -->"
|
|
1316
|
-
].join("\n")
|
|
1317
|
-
},
|
|
1318
|
-
"fsd-structure": {
|
|
1319
|
-
id: "fsd-structure",
|
|
1320
|
-
heading: "\uD504\uB860\uD2B8\uC5D4\uB4DC \uAD6C\uC870 (FSD Lite)",
|
|
1321
|
-
body: [
|
|
1322
|
-
'\uD504\uB860\uD2B8\uC5D4\uB4DC \uC571\uC740 \uC815\uC2DD Feature-Sliced Design\uC744 \uB530\uB978\uB2E4. "Lite"\uB294 \uAE30\uBCF8 \uBC94\uC704\uAC00 `app`, `pages`, `features`,',
|
|
1323
|
-
"`entities`, `shared`\uC774\uACE0(`widgets`\uB294 \uC694\uCCAD\uD560 \uB54C\uB9CC), Solo \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4\uC5D0\uC11C\uB294 \uC704\uBC18\uC744 \uC5D0\uB7EC\uAC00 \uC544\uB2CC",
|
|
1324
|
-
"\uACBD\uACE0\uB85C \uC54C\uB9B0\uB2E4\uB294 \uB73B\uC77C \uBFD0\uC774\uB2E4 \u2014 \uADDC\uCE59 \uC790\uCCB4\uB294 \uC815\uC2DD FSD \uADF8\uB300\uB85C\uB2E4:",
|
|
1325
|
-
"- \uB808\uC774\uC5B4\uB294 \uC790\uAE30\uBCF4\uB2E4 \uC544\uB798 \uB808\uC774\uC5B4\uB9CC import\uD55C\uB2E4: app > pages > widgets > features > entities > shared.",
|
|
1326
|
-
"- \uAC19\uC740 \uB808\uC774\uC5B4\uC758 \uC2AC\uB77C\uC774\uC2A4\uB07C\uB9AC(pages, widgets, features, entities)\uB294 import\uD558\uC9C0 \uC54A\uB294\uB2E4. \uACF5\uC720\uD560 \uCF54\uB4DC\uB294",
|
|
1327
|
-
" \uD55C \uB808\uC774\uC5B4 \uC544\uB798\uB85C \uB0B4\uB9AC\uAC70\uB098, \uC0C1\uB300 \uC2AC\uB77C\uC774\uC2A4\uC758 `@x/<consumer>` \uACF5\uAC1C API\uB85C\uB9CC \uB178\uCD9C\uD55C\uB2E4.",
|
|
1328
|
-
"- \uC2AC\uB77C\uC774\uC2A4\uB294 `index.ts`\uB97C \uD1B5\uD574\uC11C\uB9CC import\uD55C\uB2E4. `ui/`, `model/`, `api/` \uB0B4\uBD80 \uD30C\uC77C\uC5D0 \uC9C1\uC811 \uB2FF\uC9C0 \uC54A\uB294\uB2E4.",
|
|
1329
|
-
"- `app`\uACFC `shared`\uB294 \uC2AC\uB77C\uC774\uC2A4\uAC00 \uC544\uB2C8\uB77C \uC138\uADF8\uBA3C\uD2B8\uB97C \uAC00\uC9C4\uB2E4. shared \uC138\uADF8\uBA3C\uD2B8 \uC774\uB984\uC740 \uBAA9\uC801\uD615(`ui`, `api`,",
|
|
1330
|
-
" `model`, `lib`, `config`)\uC73C\uB85C \uC9D3\uACE0, \uBCF8\uC9C8\uD615(`components`, `hooks`, `utils`, `types`)\uC740 \uC4F0\uC9C0 \uC54A\uB294\uB2E4.",
|
|
1331
|
-
"\uC2AC\uB77C\uC774\uC2A4\uB294 generator\uB85C \uB9CC\uB4E0\uB2E4(`nx g @clearlydev/nx-archweave:page|widget|feature|entity|shared --name=\u2026`,",
|
|
1332
|
-
"\uCD94\uAC00 \uC138\uADF8\uBA3C\uD2B8\uB294 `--segments=ui,api`).",
|
|
1333
|
-
"`archweave/fsd-boundaries` ESLint \uADDC\uCE59\uC774 \uC704\uBC18\uC744 \uC54C\uB824 \uC900\uB2E4: import\uB97C \uACE0\uCE58\uB418, \uADDC\uCE59\uC744 \uB044\uAC70\uB098 eslint-disable\uB85C",
|
|
1334
|
-
"\uB118\uAE30\uC9C0 \uC54A\uB294\uB2E4. Next.js\uC5D0\uC11C\uB294 pages \uB808\uC774\uC5B4\uAC00 `_pages/` \uD3F4\uB354\uB2E4."
|
|
1335
|
-
].join("\n")
|
|
1336
|
-
},
|
|
1337
|
-
"project-goal": {
|
|
1338
|
-
id: "project-goal",
|
|
1339
|
-
heading: "\uD504\uB85C\uC81D\uD2B8 \uBAA9\uD45C",
|
|
1340
|
-
body: "<!-- TODO: \uC774 \uD504\uB85C\uC81D\uD2B8\uC758 \uBAA9\uD45C, \uB300\uC0C1 \uC0AC\uC6A9\uC790, \uC131\uACF5 \uAE30\uC900\uC744 \uC791\uC131\uD558\uC138\uC694. -->"
|
|
1341
|
-
},
|
|
1342
|
-
"architecture-boundary": {
|
|
1343
|
-
id: "architecture-boundary",
|
|
1344
|
-
heading: "\uC544\uD0A4\uD14D\uCC98 \uACBD\uACC4",
|
|
1345
|
-
body: "<!-- TODO: \uC774 \uD504\uB85C\uC81D\uD2B8\uC758 \uBAA8\uB4C8 \uACBD\uACC4, \uC18C\uC720 \uADDC\uCE59, \uC5B4\uB5A4 \uBAA8\uB4C8\uC774 \uC5B4\uB5A4 \uBAA8\uB4C8\uC5D0 \uC758\uC874\uD560 \uC218 \uC788\uB294\uC9C0\uB97C \uC791\uC131\uD558\uC138\uC694. -->"
|
|
1346
|
-
}
|
|
1347
|
-
};
|
|
1348
|
-
var AGENT_RULE_SECTIONS = {
|
|
1349
|
-
en: EN_SECTIONS,
|
|
1350
|
-
ko: KO_SECTIONS
|
|
1351
|
-
};
|
|
1352
|
-
|
|
1353
|
-
// ../templates/dist/agents/fsd-policy-notes.js
|
|
1354
|
-
var NOTES = {
|
|
1355
|
-
en: {
|
|
1356
|
-
heading: "This workspace also configures:",
|
|
1357
|
-
groups: "- A folder that only groups slices (`features/mypage/{order,claims}`) holds no code and no `index.ts`; each slice in it is separate, and slices in one group never import each other either. Create one with `--group=<name>`.",
|
|
1358
|
-
component: "- `shared` exposes each component through its own `index.ts` (`shared/ui/button`); import that, not the files inside it.",
|
|
1359
|
-
file: "- Inside `shared`, files may be imported directly (`shared/lib/format/currency`); `shared` still never imports a layer above it.",
|
|
1360
|
-
tests: "- Test files (`*.spec.ts`, `*.test.ts`, `__tests__/`) may import another slice's internals; upward imports stay forbidden there too."
|
|
1361
|
-
},
|
|
1362
|
-
ko: {
|
|
1363
|
-
heading: "\uC774 \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4\uB294 \uB2E4\uC74C\uB3C4 \uC124\uC815\uD588\uB2E4:",
|
|
1364
|
-
groups: "- \uC2AC\uB77C\uC774\uC2A4\uB97C \uBB36\uAE30\uB9CC \uD558\uB294 \uD3F4\uB354(`features/mypage/{order,claims}`)\uC5D0\uB294 \uCF54\uB4DC\uB3C4 `index.ts`\uB3C4 \uB450\uC9C0 \uC54A\uB294\uB2E4. \uADF8 \uC548\uC758 \uC2AC\uB77C\uC774\uC2A4\uB294 \uAC01\uAC01 \uB3C5\uB9BD\uC774\uB77C, \uAC19\uC740 \uADF8\uB8F9 \uC548\uC5D0\uC11C\uB3C4 \uC11C\uB85C import\uD558\uC9C0 \uC54A\uB294\uB2E4. \uADF8\uB8F9 \uC548\uC5D0 \uB9CC\uB4E4 \uB54C\uB294 `--group=<\uC774\uB984>`\uC744 \uC4F4\uB2E4.",
|
|
1365
|
-
component: "- `shared`\uB294 \uCEF4\uD3EC\uB10C\uD2B8\uB9C8\uB2E4 \uC790\uAE30 `index.ts`\uB85C \uACF5\uAC1C\uD55C\uB2E4(`shared/ui/button`). \uADF8 index\uB97C import\uD558\uACE0, \uC548\uCABD \uD30C\uC77C\uC5D0\uB294 \uB2FF\uC9C0 \uC54A\uB294\uB2E4.",
|
|
1366
|
-
file: "- `shared` \uC548\uC758 \uD30C\uC77C\uC740 \uC9C1\uC811 import\uD574\uB3C4 \uB41C\uB2E4(`shared/lib/format/currency`). \uB2E4\uB9CC `shared`\uB294 \uC5EC\uC804\uD788 \uC704 \uB808\uC774\uC5B4\uB97C import\uD558\uC9C0 \uC54A\uB294\uB2E4.",
|
|
1367
|
-
tests: "- \uD14C\uC2A4\uD2B8 \uD30C\uC77C(`*.spec.ts`, `*.test.ts`, `__tests__/`)\uC740 \uB2E4\uB978 \uC2AC\uB77C\uC774\uC2A4\uC758 \uB0B4\uBD80 \uD30C\uC77C\uC744 import\uD574\uB3C4 \uB41C\uB2E4. \uC704 \uB808\uC774\uC5B4 import\uB294 \uD14C\uC2A4\uD2B8\uC5D0\uC11C\uB3C4 \uAE08\uC9C0\uB2E4."
|
|
1368
|
-
}
|
|
1369
|
-
};
|
|
1370
|
-
function buildFsdPolicyNotes(locale, notes) {
|
|
1371
|
-
const text = NOTES[locale];
|
|
1372
|
-
const lines = [
|
|
1373
|
-
...notes.sliceGroups ? [text.groups] : [],
|
|
1374
|
-
...notes.sharedPublicApi === "component" ? [text.component] : notes.sharedPublicApi === "file" ? [text.file] : [],
|
|
1375
|
-
...notes.testFiles ? [text.tests] : []
|
|
1376
|
-
];
|
|
1377
|
-
return lines.length === 0 ? "" : [text.heading, ...lines].join("\n");
|
|
1378
|
-
}
|
|
1379
|
-
|
|
1380
|
-
// ../templates/dist/agents/module-boundaries.js
|
|
1381
|
-
function buildModuleBoundariesSection(locale, tags) {
|
|
1382
|
-
const { frontend, backend, shared } = tags;
|
|
1383
|
-
if (locale === "ko") {
|
|
1384
|
-
return {
|
|
1385
|
-
id: "module-boundaries",
|
|
1386
|
-
heading: "\uBAA8\uB4C8 \uACBD\uACC4",
|
|
1387
|
-
body: [
|
|
1388
|
-
"\uB8E8\uD2B8 ESLint \uC124\uC815\uC758 `@nx/enforce-module-boundaries` \uADDC\uCE59\uC774 \uD504\uB85C\uC81D\uD2B8 \uD0DC\uADF8\uB85C \uD504\uB860\uD2B8\uC5D4\uB4DC\uC640 \uBC31\uC5D4\uB4DC\uB97C \uB098\uB208\uB2E4:",
|
|
1389
|
-
`- \`${frontend}\` \uD504\uB85C\uC81D\uD2B8\uB294 \`${backend}\` \uD504\uB85C\uC81D\uD2B8\uB97C import\uD558\uC9C0 \uC54A\uACE0, \uADF8 \uBC18\uB300\uB3C4 \uB9C8\uCC2C\uAC00\uC9C0\uB2E4.`,
|
|
1390
|
-
`- \`${shared}\` \uD504\uB85C\uC81D\uD2B8(shared-types \uB77C\uC774\uBE0C\uB7EC\uB9AC \uB4F1)\uB294 \uC5B4\uB290 \uCABD\uB3C4 import\uD558\uC9C0 \uC54A\uB294\uB2E4.`,
|
|
1391
|
-
"- \uC774 \uD0DC\uADF8\uAC00 \uD558\uB098\uB3C4 \uC5C6\uB294 \uD504\uB85C\uC81D\uD2B8\uB294 \uADDC\uCE59 \uBC16\uC5D0 \uC788\uB2E4. \uB77C\uC774\uBE0C\uB7EC\uB9AC\uB97C \uB9CC\uB4E4 \uB54C\uB294 \uBC14\uB85C \uC790\uAE30 \uCABD \uD0DC\uADF8\uB97C \uBD99\uC778\uB2E4:",
|
|
1392
|
-
` \`nx g @nx/js:lib libs/<name> --tags=${shared}\` (\uD504\uB860\uD2B8\uC5D4\uB4DC \uC804\uC6A9\uC774\uBA74 \`${frontend}\`, \uBC31\uC5D4\uB4DC \uC804\uC6A9\uC774\uBA74 \`${backend}\`).`,
|
|
1393
|
-
`import\uB97C \uD1B5\uACFC\uC2DC\uD0A4\uB824\uACE0 \uD0DC\uADF8\uB97C \uC9C0\uC6B0\uAC70\uB098 \`depConstraints\`\uB97C \uB290\uC2A8\uD558\uAC8C \uD558\uC9C0 \uC54A\uB294\uB2E4. \uD568\uAED8 \uC4F0\uB294 \uCF54\uB4DC\uB294 \`${shared}\``,
|
|
1394
|
-
"\uB77C\uC774\uBE0C\uB7EC\uB9AC\uB85C \uC62E\uAE34\uB2E4."
|
|
1395
|
-
].join("\n")
|
|
1396
|
-
};
|
|
1397
|
-
}
|
|
1398
|
-
return {
|
|
1399
|
-
id: "module-boundaries",
|
|
1400
|
-
heading: "Module Boundaries",
|
|
1401
|
-
body: [
|
|
1402
|
-
"The root ESLint config's `@nx/enforce-module-boundaries` rule keeps the frontend and the backend apart by project tag:",
|
|
1403
|
-
`- \`${frontend}\` projects never import \`${backend}\` projects, and the other way round.`,
|
|
1404
|
-
`- \`${shared}\` projects (such as the shared-types library) import neither side.`,
|
|
1405
|
-
"- A project with none of these tags is outside the rule. When you create a library, give it its side right away:",
|
|
1406
|
-
` \`nx g @nx/js:lib libs/<name> --tags=${shared}\` (\`${frontend}\` or \`${backend}\` for code only one side uses).`,
|
|
1407
|
-
`Never remove a tag or loosen the \`depConstraints\` to make an import pass; move the shared code into a \`${shared}\``,
|
|
1408
|
-
"library instead."
|
|
1409
|
-
].join("\n")
|
|
1410
|
-
};
|
|
1411
|
-
}
|
|
1412
|
-
|
|
1413
|
-
// ../templates/dist/fsd/fsd-eslint.js
|
|
1414
|
-
var FSD_ESLINT_MODULE = "@clearlydev/nx-archweave/eslint";
|
|
1415
|
-
function buildFsdEslintImportLine() {
|
|
1416
|
-
return `import { fsdBoundaries } from '${FSD_ESLINT_MODULE}';`;
|
|
1417
|
-
}
|
|
1418
|
-
function buildFsdEslintEntry(severity, root = "src", framework = {}) {
|
|
1419
|
-
const list = (key, values) => values === void 0 || values.length === 0 ? "" : `, ${key}: [${values.map((value) => `'${value}'`).join(", ")}]`;
|
|
1420
|
-
return `...fsdBoundaries({ severity: '${severity}', cwd: import.meta.dirname, root: '${root}'${list("platformSuffixes", framework.platformSuffixes)}${list("routerDirs", framework.routerDirs)} }),`;
|
|
1421
|
-
}
|
|
1422
|
-
function buildFsdEslintSnippet({ severity, locale, root, framework }) {
|
|
1423
|
-
const lines = locale === "ko" ? [
|
|
1424
|
-
"// nx-archweave FSD \uB808\uC774\uC5B4 \uADDC\uCE59 \u2014 \uD504\uB860\uD2B8\uC5D4\uB4DC \uC571\uC758 eslint.config.mjs\uC5D0 \uC9C1\uC811 \uB123\uC73C\uC138\uC694:",
|
|
1425
|
-
"// 1) \uB9C8\uC9C0\uB9C9 import \uB4A4\uC5D0 \uC544\uB798 import\uB97C,",
|
|
1426
|
-
"// 2) \uC124\uC815 \uBC30\uC5F4(export default [...], \uB610\uB294 export default \uD558\uB294 \uBCC0\uC218\uC758 \uBC30\uC5F4)\uC758 \uB9C8\uC9C0\uB9C9 \uD56D\uBAA9\uC73C\uB85C \uC544\uB798 spread\uB97C \uCD94\uAC00\uD569\uB2C8\uB2E4."
|
|
1427
|
-
] : [
|
|
1428
|
-
"// nx-archweave FSD layer rules \u2014 add these to the frontend app's eslint.config.mjs yourself:",
|
|
1429
|
-
"// 1) the import after the last import,",
|
|
1430
|
-
"// 2) the spread as the last element of the config array (export default [...], or the array your config exports by name)."
|
|
1431
|
-
];
|
|
1432
|
-
return [...lines, buildFsdEslintImportLine(), buildFsdEslintEntry(severity, root, framework)].join("\n");
|
|
1433
|
-
}
|
|
1434
|
-
var LAYER_GUIDE_EN = {
|
|
1435
|
-
app: "composition root: providers, routing, global styles (the framework entry point lives here)",
|
|
1436
|
-
pages: "one slice per screen, composed from the layers below",
|
|
1437
|
-
features: "one slice per user-facing capability (add-to-cart, sign-in)",
|
|
1438
|
-
entities: "one slice per business object (user, product)",
|
|
1439
|
-
shared: "segments, not slices: ui, api, lib, model, config \u2014 named by purpose"
|
|
1440
|
-
};
|
|
1441
|
-
var LAYER_GUIDE_KO = {
|
|
1442
|
-
app: "\uC870\uD569 \uB8E8\uD2B8: providers, \uB77C\uC6B0\uD305, \uC804\uC5ED \uC2A4\uD0C0\uC77C (\uD504\uB808\uC784\uC6CC\uD06C \uC9C4\uC785\uC810\uC774 \uC5EC\uAE30 \uC788\uC74C)",
|
|
1443
|
-
pages: "\uD654\uBA74 \uD558\uB098\uB2F9 \uC2AC\uB77C\uC774\uC2A4 \uD558\uB098 \u2014 \uC544\uB798 \uB808\uC774\uC5B4\uB4E4\uC744 \uC870\uD569",
|
|
1444
|
-
features: "\uC0AC\uC6A9\uC790 \uAE30\uB2A5 \uD558\uB098\uB2F9 \uC2AC\uB77C\uC774\uC2A4 \uD558\uB098 (add-to-cart, sign-in)",
|
|
1445
|
-
entities: "\uBE44\uC988\uB2C8\uC2A4 \uAC1D\uCCB4 \uD558\uB098\uB2F9 \uC2AC\uB77C\uC774\uC2A4 \uD558\uB098 (user, product)",
|
|
1446
|
-
shared: "\uC2AC\uB77C\uC774\uC2A4\uAC00 \uC544\uB2C8\uB77C \uC138\uADF8\uBA3C\uD2B8: ui, api, lib, model, config \u2014 \uBAA9\uC801\uC73C\uB85C \uC774\uB984 \uC9D3\uAE30"
|
|
1447
|
-
};
|
|
1448
|
-
function buildFsdLayerGuide({ layers, directories, sourceRoot, locale }) {
|
|
1449
|
-
const guide = locale === "ko" ? LAYER_GUIDE_KO : LAYER_GUIDE_EN;
|
|
1450
|
-
const width = Math.max(...directories.map((d) => d.length));
|
|
1451
|
-
const rows = layers.map((layer, index) => ` ${sourceRoot}/${directories[index].padEnd(width)} ${guide[layer] ?? ""}`);
|
|
1452
|
-
const footer = locale === "ko" ? [
|
|
1453
|
-
"\uB808\uC774\uC5B4 \uD3F4\uB354\uB294 \uCCAB \uC2AC\uB77C\uC774\uC2A4\uAC00 \uC0DD\uAE38 \uB54C \uB9CC\uB4E4\uC5B4\uC9D1\uB2C8\uB2E4 (\uC815\uC2DD FSD):",
|
|
1454
|
-
" nx g @clearlydev/nx-archweave:feature --name=<name> (page, entity, shared\uB3C4 \uAC19\uC740 \uBC29\uC2DD; --segments=ui,api)",
|
|
1455
|
-
" widgets/ \uB808\uC774\uC5B4\uB294 \uAE30\uBCF8 \uBC94\uC704 \uBC16\uC774\uC9C0\uB9CC nx g @clearlydev/nx-archweave:widget \uC73C\uB85C \uC4F8 \uC218 \uC788\uC2B5\uB2C8\uB2E4.",
|
|
1456
|
-
"\uC704\uC5D0\uC11C \uC544\uB798\uB85C\uB9CC import\uD560 \uC218 \uC788\uACE0, \uAC19\uC740 \uB808\uC774\uC5B4\uC758 \uC2AC\uB77C\uC774\uC2A4\uB07C\uB9AC\uB294 import\uD558\uC9C0 \uC54A\uC73C\uBA70, \uC2AC\uB77C\uC774\uC2A4\uB294",
|
|
1457
|
-
"index.ts\uB97C \uD1B5\uD574\uC11C\uB9CC import\uD569\uB2C8\uB2E4 \u2014 archweave/fsd-boundaries ESLint \uADDC\uCE59\uC774 \uC704\uBC18\uC744 \uC54C\uB824 \uC90D\uB2C8\uB2E4."
|
|
1458
|
-
] : [
|
|
1459
|
-
"Layer directories appear with their first slice (canonical FSD):",
|
|
1460
|
-
" nx g @clearlydev/nx-archweave:feature --name=<name> (page, entity and shared work the same way; --segments=ui,api)",
|
|
1461
|
-
" widgets/ is outside the default scope but one command away: nx g @clearlydev/nx-archweave:widget",
|
|
1462
|
-
"Imports go downward only, slices on one layer never import each other, and a slice is",
|
|
1463
|
-
"imported through its index.ts \u2014 the archweave/fsd-boundaries ESLint rule reports violations."
|
|
1464
|
-
];
|
|
1465
|
-
return [...rows, ...footer].join("\n");
|
|
1466
|
-
}
|
|
1467
|
-
|
|
1468
|
-
// ../templates/dist/fsd/fsd-steiger.js
|
|
1469
|
-
var STEIGER_DEPENDENCIES = {
|
|
1470
|
-
steiger: "^0.7.0",
|
|
1471
|
-
"@feature-sliced/steiger-plugin": "^0.8.0"
|
|
1472
|
-
};
|
|
1473
|
-
var STEIGER_CONFIG_FILE = "steiger.config.mjs";
|
|
1474
|
-
var FSD_CHECK_TARGET = "fsd-check";
|
|
1475
|
-
function buildFsdCheckTarget(sourceRootName = "src") {
|
|
1476
|
-
return { command: `steiger ${sourceRootName}`, options: { cwd: "{projectRoot}" } };
|
|
1477
|
-
}
|
|
1478
|
-
function buildSteigerConfig({ renamedLayerDirs, locale }) {
|
|
1479
|
-
const ko = locale === "ko";
|
|
1480
|
-
const lines = [
|
|
1481
|
-
"import { defineConfig } from 'steiger';",
|
|
1482
|
-
"import fsd from '@feature-sliced/steiger-plugin';",
|
|
1483
|
-
"",
|
|
1484
|
-
ko ? "// nx-archweave: \uC815\uC2DD FSD \uADDC\uCE59 \uC804\uCCB4(recommended). \uAD6C\uC870\uB294 generator\uAC00 \uC815\uC2DD\uB300\uB85C \uB9CC\uB4E4\uBBC0\uB85C \uADF8\uB300\uB85C \uB9DE\uBB3C\uB9BD\uB2C8\uB2E4." : "// nx-archweave: the canonical rule set (recommended). The generators scaffold canonical FSD, so it fits as is.",
|
|
1485
|
-
"export default defineConfig([",
|
|
1486
|
-
" ...fsd.configs.recommended,",
|
|
1487
|
-
" {",
|
|
1488
|
-
" rules: {",
|
|
1489
|
-
ko ? " // \uC544\uBB34\uB3C4 \uCC38\uC870\uD558\uC9C0 \uC54A\uB294 \uC2AC\uB77C\uC774\uC2A4 = \uBC29\uAE08 \uC0DD\uC131\uD55C \uC2AC\uB77C\uC774\uC2A4. \uACBD\uACE0\uB85C \uBCF4\uC774\uB418 \uAC80\uC0AC\uB97C \uB9C9\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4." : " // a slice nothing references yet is every freshly generated slice: visible, not blocking",
|
|
1490
|
-
" 'fsd/insignificant-slice': 'warn',",
|
|
1491
|
-
...renamedLayerDirs.length > 0 ? [
|
|
1492
|
-
ko ? ` // ${renamedLayerDirs.join(", ")}: \uD504\uB808\uC784\uC6CC\uD06C \uB77C\uC6B0\uD130\uC640 \uACB9\uCE58\uC9C0 \uC54A\uAC8C \uC774\uB984\uC744 \uBC14\uAFBC \uB808\uC774\uC5B4 (FSD \uACF5\uC2DD \uBA85\uBA85) \u2014 steiger 0.7\uC740 \uC544\uC9C1 \uC624\uD0C0\uB85C \uBD05\uB2C8\uB2E4.` : ` // ${renamedLayerDirs.join(", ")}: layers renamed out of the framework router's way (FSD's own naming) \u2014 steiger 0.7 still reads them as typos`,
|
|
1493
|
-
" 'fsd/typo-in-layer-name': 'off',"
|
|
1494
|
-
] : [],
|
|
1495
|
-
" },",
|
|
1496
|
-
" },",
|
|
1497
|
-
"]);",
|
|
1498
|
-
""
|
|
1499
|
-
];
|
|
1500
|
-
return lines.join("\n");
|
|
1501
|
-
}
|
|
1502
|
-
|
|
1503
|
-
// ../templates/dist/backend/shared-types.js
|
|
1504
|
-
function sharedTypesIndexContent() {
|
|
1505
|
-
return `export * from './lib/api-types.js';
|
|
1506
|
-
`;
|
|
1507
|
-
}
|
|
1508
|
-
function sharedTypesApiTypesContent() {
|
|
1509
|
-
return [
|
|
1510
|
-
"// Types shared between the frontend and the backend live here.",
|
|
1511
|
-
"// Example: the api app returns this from GET /api/health, and the",
|
|
1512
|
-
"// frontend consumes the same shape \u2014 one definition, no drift.",
|
|
1513
|
-
"export interface ApiHealthResponse {",
|
|
1514
|
-
" status: 'ok';",
|
|
1515
|
-
"}",
|
|
1516
|
-
""
|
|
1517
|
-
].join("\n");
|
|
1518
|
-
}
|
|
1519
|
-
function sharedTypesPackageJsonContent(packageName, sharedTag = "scope:shared") {
|
|
1520
|
-
return `${JSON.stringify({
|
|
1521
|
-
name: packageName,
|
|
1522
|
-
version: "0.0.1",
|
|
1523
|
-
private: true,
|
|
1524
|
-
type: "module",
|
|
1525
|
-
main: "./src/index.ts",
|
|
1526
|
-
types: "./src/index.ts",
|
|
1527
|
-
exports: {
|
|
1528
|
-
".": {
|
|
1529
|
-
types: "./src/index.ts",
|
|
1530
|
-
import: "./src/index.ts",
|
|
1531
|
-
default: "./src/index.ts"
|
|
1532
|
-
}
|
|
1533
|
-
},
|
|
1534
|
-
nx: {
|
|
1535
|
-
projectType: "library",
|
|
1536
|
-
tags: [sharedTag],
|
|
1537
|
-
targets: {}
|
|
1538
|
-
}
|
|
1539
|
-
}, null, 2)}
|
|
1540
|
-
`;
|
|
1541
|
-
}
|
|
1542
|
-
|
|
1543
|
-
// ../template-engine/dist/backend/compose-backend-wiring.js
|
|
1544
|
-
var DEFAULT_SHARED_TYPES_ROOT = "libs/shared-types";
|
|
1545
|
-
function readSharedTypesConfig(config) {
|
|
1546
|
-
const value = config?.["sharedTypes"];
|
|
1547
|
-
if (value === false)
|
|
1548
|
-
return false;
|
|
1549
|
-
if (typeof value !== "object" || value === null)
|
|
1550
|
-
return { path: DEFAULT_SHARED_TYPES_ROOT };
|
|
1551
|
-
const record = value;
|
|
1552
|
-
const path = typeof record["path"] === "string" && record["path"].trim() !== "" ? record["path"].replace(/\/+$/, "") : DEFAULT_SHARED_TYPES_ROOT;
|
|
1553
|
-
const importPath = typeof record["importPath"] === "string" && record["importPath"].trim() !== "" ? record["importPath"] : void 0;
|
|
1554
|
-
return importPath === void 0 ? { path } : { path, importPath };
|
|
1555
|
-
}
|
|
1556
|
-
function composeBackendWiring(input) {
|
|
1557
|
-
const scoped = /^@([^/]+)\//.exec(input.workspaceName);
|
|
1558
|
-
const scope = scoped ? `@${scoped[1]}` : `@${input.workspaceName.split("/").pop() || "workspace"}`;
|
|
1559
|
-
const sharedTypesRoot = input.sharedTypes?.path ?? DEFAULT_SHARED_TYPES_ROOT;
|
|
1560
|
-
const sharedTypesPackageName = input.sharedTypes?.importPath ?? `${scope}/shared-types`;
|
|
1561
|
-
const tagByRole = input.tags ?? DEFAULT_BOUNDARY_TAGS;
|
|
1562
|
-
return {
|
|
1563
|
-
sharedTypesFiles: [
|
|
1564
|
-
{ filePath: `${sharedTypesRoot}/package.json`, content: sharedTypesPackageJsonContent(sharedTypesPackageName, tagByRole.shared) },
|
|
1565
|
-
{ filePath: `${sharedTypesRoot}/src/index.ts`, content: sharedTypesIndexContent() },
|
|
1566
|
-
{ filePath: `${sharedTypesRoot}/src/lib/api-types.ts`, content: sharedTypesApiTypesContent() }
|
|
1567
|
-
],
|
|
1568
|
-
sharedTypesPackageName,
|
|
1569
|
-
sharedTypesRoot,
|
|
1570
|
-
tagByRole
|
|
1571
|
-
};
|
|
1572
|
-
}
|
|
1573
|
-
function addSharedTypesNpmWorkspace(packageJsonText, entry = DEFAULT_SHARED_TYPES_ROOT) {
|
|
1574
|
-
let parsed;
|
|
1575
|
-
try {
|
|
1576
|
-
parsed = JSON.parse(packageJsonText);
|
|
1577
|
-
} catch {
|
|
1578
|
-
return { status: "unrecognized" };
|
|
1579
|
-
}
|
|
1580
|
-
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
|
|
1581
|
-
return { status: "unrecognized" };
|
|
1582
|
-
const pkg = parsed;
|
|
1583
|
-
const field = pkg["workspaces"];
|
|
1584
|
-
let target;
|
|
1585
|
-
if (field === void 0) {
|
|
1586
|
-
target = [];
|
|
1587
|
-
} else if (Array.isArray(field)) {
|
|
1588
|
-
target = field;
|
|
1589
|
-
} else if (field !== null && typeof field === "object" && Array.isArray(field.packages)) {
|
|
1590
|
-
target = field.packages;
|
|
1591
|
-
} else {
|
|
1592
|
-
return { status: "unrecognized" };
|
|
1593
|
-
}
|
|
1594
|
-
const parentGlob = `${entry.split("/").slice(0, -1).join("/")}/*`;
|
|
1595
|
-
if (target.some((existing) => existing === entry || existing === parentGlob))
|
|
1596
|
-
return { status: "present" };
|
|
1597
|
-
const surgical = insertSharedTypesWorkspace(packageJsonText, field === void 0 ? "absent" : Array.isArray(field) ? "array" : "object", JSON.stringify(entry));
|
|
1598
|
-
if (surgical !== null)
|
|
1599
|
-
return { status: "added", text: surgical };
|
|
1600
|
-
if (field === void 0)
|
|
1601
|
-
pkg["workspaces"] = target;
|
|
1602
|
-
target.push(entry);
|
|
1603
|
-
const indent = /\n([ \t]+)"/.exec(packageJsonText)?.[1] ?? " ";
|
|
1604
|
-
const trailing = packageJsonText.endsWith("\n") ? "\n" : "";
|
|
1605
|
-
return { status: "added", text: `${JSON.stringify(pkg, null, indent)}${trailing}` };
|
|
1606
|
-
}
|
|
1607
|
-
function skipJsonValue(text, start) {
|
|
1608
|
-
if (text[start] === '"') {
|
|
1609
|
-
let i = start + 1;
|
|
1610
|
-
while (i < text.length && text[i] !== '"')
|
|
1611
|
-
i += text[i] === "\\" ? 2 : 1;
|
|
1612
|
-
return i + 1;
|
|
1613
|
-
}
|
|
1614
|
-
if (text[start] === "[" || text[start] === "{") {
|
|
1615
|
-
let depth = 0;
|
|
1616
|
-
let i = start;
|
|
1617
|
-
while (i < text.length) {
|
|
1618
|
-
const c = text[i];
|
|
1619
|
-
if (c === '"') {
|
|
1620
|
-
i = skipJsonValue(text, i);
|
|
1621
|
-
continue;
|
|
1622
|
-
}
|
|
1623
|
-
if (c === "[" || c === "{")
|
|
1624
|
-
depth++;
|
|
1625
|
-
if (c === "]" || c === "}") {
|
|
1626
|
-
depth--;
|
|
1627
|
-
if (depth === 0)
|
|
1628
|
-
return i + 1;
|
|
1629
|
-
}
|
|
1630
|
-
i++;
|
|
1631
|
-
}
|
|
1632
|
-
return -1;
|
|
1633
|
-
}
|
|
1634
|
-
const literal = /^[^,}\]\s]+/.exec(text.slice(start));
|
|
1635
|
-
return literal ? start + literal[0].length : -1;
|
|
1636
|
-
}
|
|
1637
|
-
function objectMembers(text, open) {
|
|
1638
|
-
const members = [];
|
|
1639
|
-
let i = open + 1;
|
|
1640
|
-
for (; ; ) {
|
|
1641
|
-
while (/[\s,]/.test(text[i] ?? ""))
|
|
1642
|
-
i++;
|
|
1643
|
-
if (text[i] === "}")
|
|
1644
|
-
return members;
|
|
1645
|
-
if (text[i] !== '"')
|
|
1646
|
-
return null;
|
|
1647
|
-
const keyEnd = skipJsonValue(text, i);
|
|
1648
|
-
const key = JSON.parse(text.slice(i, keyEnd));
|
|
1649
|
-
i = keyEnd;
|
|
1650
|
-
while (/\s/.test(text[i] ?? ""))
|
|
1651
|
-
i++;
|
|
1652
|
-
if (text[i] !== ":")
|
|
1653
|
-
return null;
|
|
1654
|
-
i++;
|
|
1655
|
-
while (/\s/.test(text[i] ?? ""))
|
|
1656
|
-
i++;
|
|
1657
|
-
const valueEnd = skipJsonValue(text, i);
|
|
1658
|
-
if (valueEnd === -1)
|
|
1659
|
-
return null;
|
|
1660
|
-
members.push({ key, valueStart: i, valueEnd });
|
|
1661
|
-
i = valueEnd;
|
|
1662
|
-
}
|
|
1663
|
-
}
|
|
1664
|
-
function appendToArray(text, open, close, eol, quoted) {
|
|
1665
|
-
const inner = text.slice(open + 1, close - 1);
|
|
1666
|
-
if (inner.trim() === "")
|
|
1667
|
-
return `${text.slice(0, open + 1)}${quoted}${text.slice(close - 1)}`;
|
|
1668
|
-
if (!inner.includes("\n")) {
|
|
1669
|
-
const body = inner.replace(/\s+$/, "");
|
|
1670
|
-
return `${text.slice(0, open + 1)}${body}, ${quoted}${inner.slice(body.length)}${text.slice(close - 1)}`;
|
|
1671
|
-
}
|
|
1672
|
-
const lastEntryLineStart = text.lastIndexOf("\n", open + 1 + inner.trimEnd().length - 1) + 1;
|
|
1673
|
-
const itemIndent = /^[ \t]*/.exec(text.slice(lastEntryLineStart))?.[0] ?? " ";
|
|
1674
|
-
const bodyEnd = open + 1 + inner.trimEnd().length;
|
|
1675
|
-
return `${text.slice(0, bodyEnd)},${eol}${itemIndent}${quoted}${text.slice(bodyEnd)}`;
|
|
1676
|
-
}
|
|
1677
|
-
function insertSharedTypesWorkspace(text, form, quoted) {
|
|
1678
|
-
const eol = text.includes("\r\n") ? "\r\n" : "\n";
|
|
1679
|
-
const rootOpen = text.indexOf("{");
|
|
1680
|
-
if (rootOpen === -1)
|
|
1681
|
-
return null;
|
|
1682
|
-
const members = objectMembers(text, rootOpen);
|
|
1683
|
-
if (members === null)
|
|
1684
|
-
return null;
|
|
1685
|
-
const workspaces = members.find((member) => member.key === "workspaces");
|
|
1686
|
-
if (form === "absent") {
|
|
1687
|
-
const last = members[members.length - 1];
|
|
1688
|
-
const indent = /\n([ \t]+)"/.exec(text)?.[1] ?? " ";
|
|
1689
|
-
if (last === void 0) {
|
|
1690
|
-
const close = skipJsonValue(text, rootOpen) - 1;
|
|
1691
|
-
return `${text.slice(0, rootOpen + 1)}${eol}${indent}"workspaces": [${quoted}]${eol}${text.slice(close)}`;
|
|
1692
|
-
}
|
|
1693
|
-
return `${text.slice(0, last.valueEnd)},${eol}${indent}"workspaces": [${quoted}]${text.slice(last.valueEnd)}`;
|
|
1694
|
-
}
|
|
1695
|
-
if (workspaces === void 0)
|
|
1696
|
-
return null;
|
|
1697
|
-
if (form === "array") {
|
|
1698
|
-
if (text[workspaces.valueStart] !== "[")
|
|
1699
|
-
return null;
|
|
1700
|
-
return appendToArray(text, workspaces.valueStart, workspaces.valueEnd, eol, quoted);
|
|
1701
|
-
}
|
|
1702
|
-
if (text[workspaces.valueStart] !== "{")
|
|
1703
|
-
return null;
|
|
1704
|
-
const packages = objectMembers(text, workspaces.valueStart)?.find((member) => member.key === "packages");
|
|
1705
|
-
if (packages === void 0 || text[packages.valueStart] !== "[")
|
|
1706
|
-
return null;
|
|
1707
|
-
return appendToArray(text, packages.valueStart, packages.valueEnd, eol, quoted);
|
|
1708
|
-
}
|
|
1709
|
-
function addSharedTypesWorkspaceGlob(yamlText, entry = DEFAULT_SHARED_TYPES_ROOT) {
|
|
1710
|
-
const escape = (value) => value.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
|
|
1711
|
-
const parentGlob = `${entry.split("/").slice(0, -1).join("/")}/*`;
|
|
1712
|
-
if (new RegExp(`^\\s*-\\s*['"]?(${escape(entry)}|${escape(parentGlob)})['"]?\\s*$`, "m").test(yamlText)) {
|
|
1713
|
-
return { status: "present" };
|
|
1714
|
-
}
|
|
1715
|
-
const lines = yamlText.split("\n");
|
|
1716
|
-
const packagesIndex = lines.findIndex((line) => /^packages:\s*$/.test(line));
|
|
1717
|
-
if (packagesIndex === -1)
|
|
1718
|
-
return { status: "unrecognized" };
|
|
1719
|
-
let lastEntry = packagesIndex;
|
|
1720
|
-
for (let i = packagesIndex + 1; i < lines.length; i++) {
|
|
1721
|
-
if (/^\s+-\s+\S/.test(lines[i])) {
|
|
1722
|
-
lastEntry = i;
|
|
1723
|
-
} else if (lines[i].trim() === "") {
|
|
1724
|
-
continue;
|
|
1725
|
-
} else {
|
|
1726
|
-
break;
|
|
1727
|
-
}
|
|
1728
|
-
}
|
|
1729
|
-
if (lastEntry === packagesIndex)
|
|
1730
|
-
return { status: "unrecognized" };
|
|
1731
|
-
const indent = /^(\s+)-/.exec(lines[lastEntry])?.[1] ?? " ";
|
|
1732
|
-
lines.splice(lastEntry + 1, 0, `${indent}- '${entry}'`);
|
|
1733
|
-
return { status: "added", text: lines.join("\n") };
|
|
1734
|
-
}
|
|
1735
|
-
|
|
1736
|
-
// ../template-engine/dist/frameworks/profiles.js
|
|
1737
|
-
var ALL_LAYERS = ["app", "pages", "features", "entities", "shared"];
|
|
1738
|
-
var NEXT = {
|
|
1739
|
-
id: "next",
|
|
1740
|
-
recognised: true,
|
|
1741
|
-
// Next.js's own app/ (App Router) directory is the composition root; FSD's Next.js guide
|
|
1742
|
-
// names the FSD app and pages layers _app and _pages, since Next.js reads src/app and
|
|
1743
|
-
// src/pages as its routers.
|
|
1744
|
-
layers: ["pages", "features", "entities", "shared"],
|
|
1745
|
-
layerDirs: { app: "_app", pages: "_pages" },
|
|
1746
|
-
buildOutputDirs: [".next", "out"],
|
|
1747
|
-
platformSuffixes: [],
|
|
1748
|
-
routerDirs: ["pages"]
|
|
1749
|
-
};
|
|
1750
|
-
var REACT_VITE = {
|
|
1751
|
-
id: "react-vite",
|
|
1752
|
-
recognised: true,
|
|
1753
|
-
layers: ALL_LAYERS,
|
|
1754
|
-
layerDirs: {},
|
|
1755
|
-
buildOutputDirs: ["dist"],
|
|
1756
|
-
platformSuffixes: [],
|
|
1757
|
-
routerDirs: []
|
|
1758
|
-
};
|
|
1759
|
-
var EXPO_ROUTER = {
|
|
1760
|
-
id: "expo",
|
|
1761
|
-
recognised: true,
|
|
1762
|
-
layers: ALL_LAYERS,
|
|
1763
|
-
layerDirs: { app: "_app" },
|
|
1764
|
-
buildOutputDirs: [".expo", "dist"],
|
|
1765
|
-
platformSuffixes: ["ios", "android", "native", "web"],
|
|
1766
|
-
routerDirs: []
|
|
1767
|
-
};
|
|
1768
|
-
var EXPO = { ...EXPO_ROUTER, layerDirs: {} };
|
|
1769
|
-
var GENERIC = {
|
|
1770
|
-
id: "generic",
|
|
1771
|
-
recognised: false,
|
|
1772
|
-
layers: ALL_LAYERS,
|
|
1773
|
-
layerDirs: {},
|
|
1774
|
-
buildOutputDirs: ["dist", "build"],
|
|
1775
|
-
platformSuffixes: [],
|
|
1776
|
-
routerDirs: []
|
|
1777
|
-
};
|
|
1778
|
-
var PROFILES = { next: NEXT, "react-vite": REACT_VITE, expo: EXPO_ROUTER, generic: GENERIC };
|
|
1779
|
-
function profileById(id) {
|
|
1780
|
-
return PROFILES[id];
|
|
1781
|
-
}
|
|
1782
|
-
function allFrameworkProfiles() {
|
|
1783
|
-
return Object.values(PROFILES);
|
|
1784
|
-
}
|
|
1785
|
-
var NEXT_CONFIGS = ["next.config.js", "next.config.mjs", "next.config.ts", "next.config.cjs"];
|
|
1786
|
-
var VITE_CONFIGS = ["vite.config.ts", "vite.config.mts", "vite.config.js", "vite.config.mjs"];
|
|
1787
|
-
var EXPO_CONFIGS = ["app.config.ts", "app.config.js", "app.config.mjs"];
|
|
1788
|
-
function hasExpoKey(input, file) {
|
|
1789
|
-
const text = input.read(file);
|
|
1790
|
-
if (typeof text !== "string")
|
|
1791
|
-
return false;
|
|
1792
|
-
try {
|
|
1793
|
-
return typeof JSON.parse(text)["expo"] === "object";
|
|
1794
|
-
} catch {
|
|
1795
|
-
return false;
|
|
1796
|
-
}
|
|
1797
|
-
}
|
|
1798
|
-
function dependsOn(input, manifest, name) {
|
|
1799
|
-
const text = input.read(manifest);
|
|
1800
|
-
if (typeof text !== "string")
|
|
1801
|
-
return false;
|
|
1802
|
-
try {
|
|
1803
|
-
const parsed = JSON.parse(text);
|
|
1804
|
-
return ["dependencies", "devDependencies", "peerDependencies"].some((field) => parsed[field]?.[name] !== void 0);
|
|
1805
|
-
} catch {
|
|
1806
|
-
return false;
|
|
1807
|
-
}
|
|
1808
|
-
}
|
|
1809
|
-
function detectFramework(input) {
|
|
1810
|
-
const prefix = input.projectRoot === "" || input.projectRoot === "." ? "" : `${input.projectRoot}/`;
|
|
1811
|
-
const executors = input.executors ?? [];
|
|
1812
|
-
if (NEXT_CONFIGS.some((file) => input.exists(`${prefix}${file}`)) || executors.some((executor) => executor.startsWith("@nx/next:"))) {
|
|
1813
|
-
return NEXT;
|
|
1814
|
-
}
|
|
1815
|
-
const dependsAnywhere = (name) => dependsOn(input, `${prefix}package.json`, name) || dependsOn(input, "package.json", name);
|
|
1816
|
-
const usesExpo = executors.some((executor) => executor.startsWith("@nx/expo:")) || hasExpoKey(input, `${prefix}app.json`) || EXPO_CONFIGS.some((file) => input.exists(`${prefix}${file}`)) || dependsOn(input, `${prefix}package.json`, "expo");
|
|
1817
|
-
if (usesExpo)
|
|
1818
|
-
return dependsAnywhere("expo-router") ? EXPO_ROUTER : EXPO;
|
|
1819
|
-
const usesReact = dependsAnywhere("react");
|
|
1820
|
-
const usesVite = VITE_CONFIGS.some((file) => input.exists(`${prefix}${file}`)) || executors.some((executor) => executor.startsWith("@nx/vite:"));
|
|
1821
|
-
if (usesVite && usesReact)
|
|
1822
|
-
return REACT_VITE;
|
|
1823
|
-
return GENERIC;
|
|
1824
|
-
}
|
|
1825
|
-
|
|
1826
|
-
// ../template-engine/dist/fsd/fsd-eslint-config.js
|
|
1827
|
-
function classifyFsdEslintConfig(configText) {
|
|
1828
|
-
return maskComments(configText).includes(FSD_ESLINT_MODULE) ? "applied" : "absent";
|
|
1829
|
-
}
|
|
1830
|
-
var EXPORT_DEFAULT_ARRAY = /export\s+default\s+\[/g;
|
|
1831
|
-
var IMPORT_STATEMENT = /^import\b[^;]*?\bfrom\s+['"][^'"]+['"]\s*;?[ \t]*$/gm;
|
|
1832
|
-
var CLOSING_BRACKET = /\]\s*;?\s*$/;
|
|
1833
|
-
function closingBracketOf(masked, open) {
|
|
1834
|
-
let depth = 0;
|
|
1835
|
-
for (let i = open; i < masked.length; i++) {
|
|
1836
|
-
const ch = masked[i];
|
|
1837
|
-
if (ch === '"' || ch === "'" || ch === "`") {
|
|
1838
|
-
for (i++; i < masked.length && masked[i] !== ch; i++)
|
|
1839
|
-
if (masked[i] === "\\")
|
|
1840
|
-
i++;
|
|
1841
|
-
continue;
|
|
1842
|
-
}
|
|
1843
|
-
if (ch === "[")
|
|
1844
|
-
depth++;
|
|
1845
|
-
else if (ch === "]" && --depth === 0)
|
|
1846
|
-
return i;
|
|
1847
|
-
}
|
|
1848
|
-
return -1;
|
|
1849
|
-
}
|
|
1850
|
-
function namedConfigArrayEnd(masked) {
|
|
1851
|
-
const exported = [...masked.matchAll(/export\s+default\s+([A-Za-z_$][\w$]*)\s*;?/g)];
|
|
1852
|
-
if (exported.length !== 1)
|
|
1853
|
-
return -1;
|
|
1854
|
-
const declaration = new RegExp(`(?:const|let|var)\\s+${exported[0][1].replace(/\$/g, "\\$")}\\s*=\\s*(?:defineConfig\\s*\\(\\s*)?\\[`).exec(masked);
|
|
1855
|
-
if (declaration === null)
|
|
1856
|
-
return -1;
|
|
1857
|
-
return closingBracketOf(masked, declaration.index + declaration[0].length - 1);
|
|
1858
|
-
}
|
|
1859
|
-
function applyFsdEslintConfig(configText, severity, root = "src", framework = {}) {
|
|
1860
|
-
if (classifyFsdEslintConfig(configText) === "applied")
|
|
1861
|
-
return { status: "already-applied" };
|
|
1862
|
-
const masked = maskComments(configText);
|
|
1863
|
-
EXPORT_DEFAULT_ARRAY.lastIndex = 0;
|
|
1864
|
-
const exportMatches = masked.match(EXPORT_DEFAULT_ARRAY) ?? [];
|
|
1865
|
-
const closing = CLOSING_BRACKET.exec(masked);
|
|
1866
|
-
const closingIndex = exportMatches.length === 1 && closing !== null ? closing.index : namedConfigArrayEnd(masked);
|
|
1867
|
-
if (closingIndex === -1)
|
|
1868
|
-
return { status: "unrecognized" };
|
|
1869
|
-
const eol = configText.includes("\r\n") ? "\r\n" : "\n";
|
|
1870
|
-
const before = configText.slice(0, closingIndex);
|
|
1871
|
-
const lastNonSpace = before.replace(/\s+$/, "");
|
|
1872
|
-
const needsComma = !lastNonSpace.endsWith(",") && !lastNonSpace.endsWith("[");
|
|
1873
|
-
const entryText = `${needsComma ? "," : ""}${eol} ${buildFsdEslintEntry(severity, root, framework)}${eol}`;
|
|
1874
|
-
const withEntry = `${lastNonSpace}${entryText}${configText.slice(closingIndex)}`;
|
|
1875
|
-
IMPORT_STATEMENT.lastIndex = 0;
|
|
1876
|
-
let lastImportEnd = -1;
|
|
1877
|
-
const maskedWithEntry = maskComments(withEntry);
|
|
1878
|
-
for (let m = IMPORT_STATEMENT.exec(maskedWithEntry); m !== null; m = IMPORT_STATEMENT.exec(maskedWithEntry)) {
|
|
1879
|
-
lastImportEnd = m.index + m[0].length;
|
|
1880
|
-
}
|
|
1881
|
-
const importLine = buildFsdEslintImportLine();
|
|
1882
|
-
const text = lastImportEnd === -1 ? `${importLine}${eol}${eol}${withEntry}` : `${withEntry.slice(0, lastImportEnd)}${eol}${importLine}${withEntry.slice(lastImportEnd)}`;
|
|
1883
|
-
return { status: "applied", text };
|
|
1884
|
-
}
|
|
1885
|
-
function buildFsdEslintPasteSnippet(severity, locale, root = "src", framework = {}) {
|
|
1886
|
-
return buildFsdEslintSnippet({ severity, locale, root, framework });
|
|
1887
|
-
}
|
|
1888
|
-
function composeFsdLayerGuide({ profile, sourceRoot, locale }) {
|
|
1889
|
-
const layers = [...profile.layers];
|
|
1890
|
-
return buildFsdLayerGuide({ layers, directories: layers.map((layer) => profile.layerDirs[layer] ?? layer), sourceRoot, locale });
|
|
1891
|
-
}
|
|
1892
|
-
|
|
1893
|
-
// ../template-engine/dist/ai-hosts/ai-hosts.js
|
|
1894
|
-
var AI_HOSTS = [
|
|
1895
|
-
{ id: "claude-code", label: "Claude Code", nxAgent: "claude", rulesFile: "CLAUDE.md", reviewProvider: "claude" },
|
|
1896
|
-
{ id: "codex", label: "Codex", nxAgent: "codex", rulesFile: "AGENTS.md", reviewProvider: "gpt" },
|
|
1897
|
-
{ id: "cursor", label: "Cursor", nxAgent: "cursor", rulesFile: "AGENTS.md", reviewProvider: "cursor" },
|
|
1898
|
-
{ id: "gemini-cli", label: "Gemini CLI", nxAgent: "gemini", rulesFile: "AGENTS.md", reviewProvider: "gemini" }
|
|
1899
|
-
];
|
|
1900
|
-
var AI_HOST_IDS = AI_HOSTS.map((host) => host.id);
|
|
1901
|
-
var DEFAULT_AI_HOSTS = ["claude-code"];
|
|
1902
|
-
function isAiHost(value) {
|
|
1903
|
-
return typeof value === "string" && AI_HOST_IDS.includes(value);
|
|
1904
|
-
}
|
|
1905
|
-
function definitionOf(id) {
|
|
1906
|
-
const host = AI_HOSTS.find((candidate) => candidate.id === id);
|
|
1907
|
-
if (!host)
|
|
1908
|
-
throw new Error(`Unknown AI host "${id}"`);
|
|
1909
|
-
return host;
|
|
1910
|
-
}
|
|
1911
|
-
function rulesFilesFor(hosts) {
|
|
1912
|
-
const picked = hosts.length > 0 ? hosts : DEFAULT_AI_HOSTS;
|
|
1913
|
-
const files = new Set(picked.map((id) => definitionOf(id).rulesFile));
|
|
1914
|
-
return ["CLAUDE.md", "AGENTS.md"].filter((file) => files.has(file));
|
|
1915
|
-
}
|
|
1916
|
-
function reviewProviderFor(hosts) {
|
|
1917
|
-
return definitionOf(hosts[0] ?? DEFAULT_AI_HOSTS[0]).reviewProvider;
|
|
1918
|
-
}
|
|
70
|
+
import { join } from "node:path";
|
|
1919
71
|
|
|
1920
72
|
// ../template-engine/dist/presets/preset-config.js
|
|
1921
73
|
function buildFsdConfig(enforcement, frontend) {
|
|
@@ -2002,1466 +154,6 @@ function buildPresetConfig(input) {
|
|
|
2002
154
|
};
|
|
2003
155
|
}
|
|
2004
156
|
|
|
2005
|
-
// ../template-engine/dist/agent-rules/compose-agent-rules.js
|
|
2006
|
-
function resolveDefaultAgentSectionIds(teamRules, fsdStructure = false) {
|
|
2007
|
-
let ids = [...DEFAULT_AGENT_SECTION_IDS];
|
|
2008
|
-
const insertAfter = (anchorId, id) => {
|
|
2009
|
-
const anchor = ids.indexOf(anchorId);
|
|
2010
|
-
if (anchor === -1) {
|
|
2011
|
-
throw new Error(`composeAgentRules: the default composition no longer contains ${anchorId}`);
|
|
2012
|
-
}
|
|
2013
|
-
ids = [...ids.slice(0, anchor + 1), id, ...ids.slice(anchor + 1)];
|
|
2014
|
-
};
|
|
2015
|
-
if (fsdStructure)
|
|
2016
|
-
insertAfter("code-structure", FSD_AGENT_SECTION_ID);
|
|
2017
|
-
if (teamRules)
|
|
2018
|
-
insertAfter("language-policy", TEAM_AGENT_SECTION_ID);
|
|
2019
|
-
return ids;
|
|
2020
|
-
}
|
|
2021
|
-
function renderSections(sections) {
|
|
2022
|
-
return `${sections.map((section) => `## ${section.heading}
|
|
2023
|
-
|
|
2024
|
-
${section.body}`).join("\n\n")}
|
|
2025
|
-
`;
|
|
2026
|
-
}
|
|
2027
|
-
function composeAgentRules(input) {
|
|
2028
|
-
const sectionIds = input.sections ?? resolveDefaultAgentSectionIds(input.teamRules ?? false, input.fsdStructure ?? false);
|
|
2029
|
-
const notes = input.fsdPolicy === void 0 ? "" : buildFsdPolicyNotes(input.locale, input.fsdPolicy);
|
|
2030
|
-
const sections = sectionIds.map((id) => {
|
|
2031
|
-
const section = AGENT_RULE_SECTIONS[input.locale][id];
|
|
2032
|
-
return id === FSD_AGENT_SECTION_ID && notes !== "" ? { ...section, body: `${section.body}
|
|
2033
|
-
|
|
2034
|
-
${notes}` } : section;
|
|
2035
|
-
});
|
|
2036
|
-
if (input.moduleBoundaries !== void 0 && input.sections === void 0) {
|
|
2037
|
-
const anchor = sections.findIndex((section) => section.id === FSD_AGENT_SECTION_ID);
|
|
2038
|
-
const at = (anchor === -1 ? sections.findIndex((section) => section.id === "code-structure") : anchor) + 1;
|
|
2039
|
-
sections.splice(at, 0, buildModuleBoundariesSection(input.locale, input.moduleBoundaries));
|
|
2040
|
-
}
|
|
2041
|
-
const content = renderSections(sections);
|
|
2042
|
-
return input.files.map((filePath) => ({ filePath, content }));
|
|
2043
|
-
}
|
|
2044
|
-
|
|
2045
|
-
// ../template-engine/dist/agent-rules/validate-agent-rules.js
|
|
2046
|
-
var LOCALES = ["en", "ko"];
|
|
2047
|
-
function headingsFor(id) {
|
|
2048
|
-
return LOCALES.map((locale) => `## ${AGENT_RULE_SECTIONS[locale][id].heading}`);
|
|
2049
|
-
}
|
|
2050
|
-
var REQUIRED_SECTION_IDS = ["safety-rules", "language-policy"];
|
|
2051
|
-
function validateAgentRules(content, extraRequired = []) {
|
|
2052
|
-
return [...REQUIRED_SECTION_IDS, ...extraRequired].every((id) => headingsFor(id).some((heading) => content.includes(heading)));
|
|
2053
|
-
}
|
|
2054
|
-
|
|
2055
|
-
// ../template-engine/dist/agent-rules/marker-block.js
|
|
2056
|
-
import { createHash } from "node:crypto";
|
|
2057
|
-
var AGENT_RULES_BLOCK_START = "<!-- nx-archweave:agent-rules start -->";
|
|
2058
|
-
var AGENT_RULES_BLOCK_END = "<!-- nx-archweave:agent-rules end -->";
|
|
2059
|
-
var START_MARKER = /<!-- nx-archweave:agent-rules start(?: hash=([0-9a-f]{12}))? -->/g;
|
|
2060
|
-
function normalizeForHash(text) {
|
|
2061
|
-
return text.replace(/\r\n/g, "\n").replace(/[ \t]+$/gm, "");
|
|
2062
|
-
}
|
|
2063
|
-
function hashOf(body) {
|
|
2064
|
-
return createHash("sha256").update(normalizeForHash(body)).digest("hex").slice(0, 12);
|
|
2065
|
-
}
|
|
2066
|
-
function withFinalNewline(body) {
|
|
2067
|
-
return body.endsWith("\n") ? body : `${body}
|
|
2068
|
-
`;
|
|
2069
|
-
}
|
|
2070
|
-
function renderAgentRulesBlock(body, eol = "\n") {
|
|
2071
|
-
const content = withFinalNewline(normalizeForHash(body));
|
|
2072
|
-
const lines = `<!-- nx-archweave:agent-rules start hash=${hashOf(content)} -->
|
|
2073
|
-
${content}${AGENT_RULES_BLOCK_END}`;
|
|
2074
|
-
return eol === "\n" ? lines : lines.replace(/\r?\n/g, eol);
|
|
2075
|
-
}
|
|
2076
|
-
function countOf(text, needle) {
|
|
2077
|
-
return text.split(needle).length - 1;
|
|
2078
|
-
}
|
|
2079
|
-
function applyAgentRulesBlock(existing, body, options = {}) {
|
|
2080
|
-
if (existing === null || existing.length === 0)
|
|
2081
|
-
return { kind: "create", text: `${renderAgentRulesBlock(body)}
|
|
2082
|
-
` };
|
|
2083
|
-
const eol = existing.includes("\r\n") ? "\r\n" : "\n";
|
|
2084
|
-
const block = renderAgentRulesBlock(body, eol);
|
|
2085
|
-
const starts = [...existing.matchAll(START_MARKER)];
|
|
2086
|
-
const ends = countOf(existing, AGENT_RULES_BLOCK_END);
|
|
2087
|
-
if (starts.length === 0 && ends === 0) {
|
|
2088
|
-
const head = existing.endsWith("\n") ? existing : `${existing}${eol}`;
|
|
2089
|
-
return { kind: "append", text: `${head}${eol}${block}${eol}` };
|
|
2090
|
-
}
|
|
2091
|
-
if (starts.length !== 1 || ends !== 1) {
|
|
2092
|
-
return {
|
|
2093
|
-
kind: "unrecognized",
|
|
2094
|
-
reason: `expected one ${AGENT_RULES_BLOCK_START} \u2026 ${AGENT_RULES_BLOCK_END} pair, found ${starts.length} start and ${ends} end marker(s)`
|
|
2095
|
-
};
|
|
2096
|
-
}
|
|
2097
|
-
const startMatch = starts[0];
|
|
2098
|
-
const start = startMatch.index ?? 0;
|
|
2099
|
-
const end = existing.indexOf(AGENT_RULES_BLOCK_END);
|
|
2100
|
-
if (end < start)
|
|
2101
|
-
return { kind: "unrecognized", reason: "the end marker comes before the start marker" };
|
|
2102
|
-
const text = existing.slice(0, start) + block + existing.slice(end + AGENT_RULES_BLOCK_END.length);
|
|
2103
|
-
if (text === existing)
|
|
2104
|
-
return { kind: "unchanged" };
|
|
2105
|
-
const bodyStart = start + startMatch[0].length + (existing.startsWith("\r\n", start + startMatch[0].length) ? 2 : 1);
|
|
2106
|
-
const existingBody = normalizeForHash(existing.slice(bodyStart, end));
|
|
2107
|
-
const generatedBody = withFinalNewline(normalizeForHash(body));
|
|
2108
|
-
const recordedHash = startMatch[1];
|
|
2109
|
-
const untouched = recordedHash !== void 0 ? hashOf(existingBody) === recordedHash : existingBody === generatedBody;
|
|
2110
|
-
if (!untouched && options.overwrite !== true)
|
|
2111
|
-
return { kind: "user-modified", existingBody, generatedBody };
|
|
2112
|
-
return { kind: "replace", text };
|
|
2113
|
-
}
|
|
2114
|
-
|
|
2115
|
-
// src/generators/add-fsd-lite/generator.ts
|
|
2116
|
-
import { posix } from "node:path";
|
|
2117
|
-
|
|
2118
|
-
// src/generators/shared/dry-run.ts
|
|
2119
|
-
function resolveDryRun(options) {
|
|
2120
|
-
return process.env["NX_DRY_RUN"] === "true" || (options.dryRun ?? false);
|
|
2121
|
-
}
|
|
2122
|
-
|
|
2123
|
-
// src/generators/shared/frontend-app.ts
|
|
2124
|
-
import { getProjects } from "@nx/devkit";
|
|
2125
|
-
|
|
2126
|
-
// src/generators/shared/json-edit.ts
|
|
2127
|
-
import { findNodeAtLocation, parseTree } from "jsonc-parser";
|
|
2128
|
-
function detectIndent(raw) {
|
|
2129
|
-
for (const line of raw.split("\n")) {
|
|
2130
|
-
const match = /^(\t+| +)/.exec(line);
|
|
2131
|
-
if (match) return match[1];
|
|
2132
|
-
}
|
|
2133
|
-
return " ";
|
|
2134
|
-
}
|
|
2135
|
-
function styleOf(raw) {
|
|
2136
|
-
return { eol: raw.includes("\r\n") ? "\r\n" : "\n", unit: detectIndent(raw) };
|
|
2137
|
-
}
|
|
2138
|
-
function lineIndentAt(raw, offset) {
|
|
2139
|
-
const lineStart = raw.lastIndexOf("\n", offset - 1) + 1;
|
|
2140
|
-
return /^[ \t]*/.exec(raw.slice(lineStart))?.[0] ?? "";
|
|
2141
|
-
}
|
|
2142
|
-
function isMultiline(raw, node) {
|
|
2143
|
-
return raw.slice(node.offset, node.offset + node.length).includes("\n");
|
|
2144
|
-
}
|
|
2145
|
-
function isPrimitive(value) {
|
|
2146
|
-
return value === null || ["string", "number", "boolean"].includes(typeof value);
|
|
2147
|
-
}
|
|
2148
|
-
function renderValue(value, indent, style) {
|
|
2149
|
-
if (isPrimitive(value)) return JSON.stringify(value);
|
|
2150
|
-
if (Array.isArray(value)) {
|
|
2151
|
-
if (value.every(isPrimitive)) return `[${value.map((item) => JSON.stringify(item)).join(", ")}]`;
|
|
2152
|
-
const inner2 = `${indent}${style.unit}`;
|
|
2153
|
-
return `[${style.eol}${value.map((item) => `${inner2}${renderValue(item, inner2, style)}`).join(`,${style.eol}`)}${style.eol}${indent}]`;
|
|
2154
|
-
}
|
|
2155
|
-
const entries = Object.entries(value);
|
|
2156
|
-
if (entries.length === 0) return "{}";
|
|
2157
|
-
const inner = `${indent}${style.unit}`;
|
|
2158
|
-
const body = entries.map(([key, item]) => `${inner}${JSON.stringify(key)}: ${renderValue(item, inner, style)}`).join(`,${style.eol}`);
|
|
2159
|
-
return `{${style.eol}${body}${style.eol}${indent}}`;
|
|
2160
|
-
}
|
|
2161
|
-
function splice(raw, start, end, text) {
|
|
2162
|
-
return raw.slice(0, start) + text + raw.slice(end);
|
|
2163
|
-
}
|
|
2164
|
-
function nest(keys, value) {
|
|
2165
|
-
return keys.reduceRight((inner, key) => ({ [key]: inner }), value);
|
|
2166
|
-
}
|
|
2167
|
-
function insertProperty(raw, object, key, value, style) {
|
|
2168
|
-
const properties = object.children ?? [];
|
|
2169
|
-
const objectIndent = lineIndentAt(raw, object.offset);
|
|
2170
|
-
if (properties.length === 0) {
|
|
2171
|
-
const inner = `${objectIndent}${style.unit}`;
|
|
2172
|
-
const text = `{${style.eol}${inner}${JSON.stringify(String(key))}: ${renderValue(value, inner, style)}${style.eol}${objectIndent}}`;
|
|
2173
|
-
return splice(raw, object.offset, object.offset + object.length, text);
|
|
2174
|
-
}
|
|
2175
|
-
const last = properties[properties.length - 1];
|
|
2176
|
-
const end = last.offset + last.length;
|
|
2177
|
-
if (!isMultiline(raw, object)) {
|
|
2178
|
-
return splice(raw, end, end, `, ${JSON.stringify(String(key))}: ${renderValue(value, "", style)}`);
|
|
2179
|
-
}
|
|
2180
|
-
const indent = lineIndentAt(raw, last.offset);
|
|
2181
|
-
return splice(raw, end, end, `,${style.eol}${indent}${JSON.stringify(String(key))}: ${renderValue(value, indent, style)}`);
|
|
2182
|
-
}
|
|
2183
|
-
function setJsonValue(raw, path, value) {
|
|
2184
|
-
const style = styleOf(raw);
|
|
2185
|
-
const root = parseTree(raw);
|
|
2186
|
-
if (root === void 0) throw new Error("setJsonValue: not a JSON document");
|
|
2187
|
-
const existing = findNodeAtLocation(root, path);
|
|
2188
|
-
if (existing !== void 0) {
|
|
2189
|
-
return splice(raw, existing.offset, existing.offset + existing.length, renderValue(value, lineIndentAt(raw, existing.offset), style));
|
|
2190
|
-
}
|
|
2191
|
-
for (let depth = path.length - 1; depth >= 0; depth--) {
|
|
2192
|
-
const parent = depth === 0 ? root : findNodeAtLocation(root, path.slice(0, depth));
|
|
2193
|
-
if (parent === void 0) continue;
|
|
2194
|
-
if (parent.type !== "object") throw new Error(`setJsonValue: ${path.slice(0, depth).join(".") || "the root"} is not an object`);
|
|
2195
|
-
return insertProperty(raw, parent, path[depth], nest(path.slice(depth + 1), value), style);
|
|
2196
|
-
}
|
|
2197
|
-
throw new Error("setJsonValue: unreachable");
|
|
2198
|
-
}
|
|
2199
|
-
function appendJsonArrayItem(raw, path, item) {
|
|
2200
|
-
const style = styleOf(raw);
|
|
2201
|
-
const root = parseTree(raw);
|
|
2202
|
-
const array = root === void 0 ? void 0 : findNodeAtLocation(root, path);
|
|
2203
|
-
if (array === void 0) return setJsonValue(raw, path, [item]);
|
|
2204
|
-
if (array.type !== "array") throw new Error(`appendJsonArrayItem: ${path.join(".")} is not an array`);
|
|
2205
|
-
const items = array.children ?? [];
|
|
2206
|
-
if (items.length === 0) return splice(raw, array.offset, array.offset + array.length, renderValue([item], "", style));
|
|
2207
|
-
const last = items[items.length - 1];
|
|
2208
|
-
const end = last.offset + last.length;
|
|
2209
|
-
if (!isMultiline(raw, array)) {
|
|
2210
|
-
const separator = items.length >= 2 ? raw.slice(items[0].offset + items[0].length, items[1].offset) : ", ";
|
|
2211
|
-
return splice(raw, end, end, `${separator}${renderValue(item, "", style)}`);
|
|
2212
|
-
}
|
|
2213
|
-
const indent = lineIndentAt(raw, last.offset);
|
|
2214
|
-
return splice(raw, end, end, `,${style.eol}${indent}${renderValue(item, indent, style)}`);
|
|
2215
|
-
}
|
|
2216
|
-
function applyJsonEdits(raw, edits) {
|
|
2217
|
-
if (raw === null) {
|
|
2218
|
-
let text = "{}\n";
|
|
2219
|
-
for (const [path, value] of edits) text = setJsonValue(text, path, value);
|
|
2220
|
-
return text;
|
|
2221
|
-
}
|
|
2222
|
-
return edits.reduce((text, [path, value]) => setJsonValue(text, path, value), raw);
|
|
2223
|
-
}
|
|
2224
|
-
|
|
2225
|
-
// src/generators/shared/toolkit-config-file.ts
|
|
2226
|
-
var TOOLKIT_CONFIG_PATH = "toolkit.config.json";
|
|
2227
|
-
function readToolkitConfigFile(tree) {
|
|
2228
|
-
const raw = tree.read(TOOLKIT_CONFIG_PATH, "utf-8");
|
|
2229
|
-
if (raw === null) return { raw: null, parsed: null };
|
|
2230
|
-
try {
|
|
2231
|
-
const parsed = JSON.parse(raw);
|
|
2232
|
-
return {
|
|
2233
|
-
raw,
|
|
2234
|
-
parsed: typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : null
|
|
2235
|
-
};
|
|
2236
|
-
} catch {
|
|
2237
|
-
return { raw, parsed: null };
|
|
2238
|
-
}
|
|
2239
|
-
}
|
|
2240
|
-
function readToolkitConfig(tree) {
|
|
2241
|
-
return readToolkitConfigFile(tree).parsed;
|
|
2242
|
-
}
|
|
2243
|
-
function resolveLocaleOption(option, config) {
|
|
2244
|
-
if (option) return option;
|
|
2245
|
-
const locale = config?.["locale"];
|
|
2246
|
-
return locale === "ko" || locale === "en" ? locale : "en";
|
|
2247
|
-
}
|
|
2248
|
-
function recordedChoice(config, key, allowed) {
|
|
2249
|
-
const value = config?.[key];
|
|
2250
|
-
return typeof value === "string" && allowed.includes(value) ? value : void 0;
|
|
2251
|
-
}
|
|
2252
|
-
var RECORDED_FRONTENDS = ["react-vite", "next", "expo", "generic", "none"];
|
|
2253
|
-
var RECORDED_BACKENDS = ["express", "nest", "none"];
|
|
2254
|
-
var RECORDED_PRESETS = ["quick-start", "solo", "team"];
|
|
2255
|
-
var RECORDED_PACKAGE_MANAGERS = ["pnpm", "npm", "yarn", "bun"];
|
|
2256
|
-
function resolveFrontendOption(option, config, generator) {
|
|
2257
|
-
const frontend = option ?? recordedChoice(config, "frontend", RECORDED_FRONTENDS);
|
|
2258
|
-
if (frontend === void 0) {
|
|
2259
|
-
throw new Error(`${generator}: pass --frontend=react-vite|next|expo|generic (toolkit.config.json records none \u2014 init writes it).`);
|
|
2260
|
-
}
|
|
2261
|
-
if (frontend === "none") {
|
|
2262
|
-
throw new Error(
|
|
2263
|
-
`${generator}: this is a backend-only workspace (toolkit.config.json frontend: none) \u2014 FSD structures a frontend app. Pass --frontend=react-vite|next|expo|generic if you added one.`
|
|
2264
|
-
);
|
|
2265
|
-
}
|
|
2266
|
-
return frontend;
|
|
2267
|
-
}
|
|
2268
|
-
function resolveInitFrontend(option, config) {
|
|
2269
|
-
const frontend = option ?? recordedChoice(config, "frontend", RECORDED_FRONTENDS);
|
|
2270
|
-
if (frontend === void 0) {
|
|
2271
|
-
throw new Error("init: pass --frontend=react-vite|next|expo|generic|none (toolkit.config.json records none \u2014 init writes it).");
|
|
2272
|
-
}
|
|
2273
|
-
return frontend;
|
|
2274
|
-
}
|
|
2275
|
-
function workspacePackageManager(exists, read, config) {
|
|
2276
|
-
return detectWorkspacePackageManager(exists, read) ?? recordedChoice(config, "packageManager", RECORDED_PACKAGE_MANAGERS) ?? null;
|
|
2277
|
-
}
|
|
2278
|
-
function treePackageManager(tree, config) {
|
|
2279
|
-
return workspacePackageManager((p) => tree.exists(p), (p) => tree.read(p, "utf-8"), config);
|
|
2280
|
-
}
|
|
2281
|
-
function resolveAiHostsOption(option, config) {
|
|
2282
|
-
if (option !== void 0 && option.length > 0) return [...new Set(option)];
|
|
2283
|
-
const recorded = config?.["aiHosts"];
|
|
2284
|
-
const fromConfig = Array.isArray(recorded) ? [...new Set(recorded.filter(isAiHost))] : [];
|
|
2285
|
-
return fromConfig.length > 0 ? fromConfig : [...DEFAULT_AI_HOSTS];
|
|
2286
|
-
}
|
|
2287
|
-
function recordAiHosts(tree, hosts, write) {
|
|
2288
|
-
const { raw, parsed } = readToolkitConfigFile(tree);
|
|
2289
|
-
if (raw !== null && parsed === null) return "unrecognized";
|
|
2290
|
-
const existing = parsed ?? {};
|
|
2291
|
-
const recorded = existing["aiHosts"];
|
|
2292
|
-
if (Array.isArray(recorded) && recorded.length === hosts.length && recorded.every((id, index) => id === hosts[index])) {
|
|
2293
|
-
return "unchanged";
|
|
2294
|
-
}
|
|
2295
|
-
if (write) tree.write(TOOLKIT_CONFIG_PATH, applyJsonEdits(raw, [[["aiHosts"], [...hosts]]]));
|
|
2296
|
-
return raw === null ? "created" : "updated";
|
|
2297
|
-
}
|
|
2298
|
-
function recordedFrontends(config) {
|
|
2299
|
-
const frontends = config?.["frontends"];
|
|
2300
|
-
if (!Array.isArray(frontends)) return [];
|
|
2301
|
-
return frontends.flatMap(
|
|
2302
|
-
(entry) => typeof entry === "object" && entry !== null && typeof entry["project"] === "string" ? [entry.project] : []
|
|
2303
|
-
);
|
|
2304
|
-
}
|
|
2305
|
-
|
|
2306
|
-
// src/generators/shared/project-type.ts
|
|
2307
|
-
import { parseJson } from "@nx/devkit";
|
|
2308
|
-
function resolveProjectType(tree, root, declared) {
|
|
2309
|
-
if (declared) return declared === "library" ? "library" : "application";
|
|
2310
|
-
const prefix = root === "" || root === "." ? "" : `${root}/`;
|
|
2311
|
-
if (tree.exists(`${prefix}tsconfig.lib.json`)) return "library";
|
|
2312
|
-
if (tree.exists(`${prefix}tsconfig.app.json`)) return "application";
|
|
2313
|
-
const raw = tree.read(`${prefix}package.json`, "utf-8");
|
|
2314
|
-
if (raw === null) return "library";
|
|
2315
|
-
try {
|
|
2316
|
-
const manifest = parseJson(raw);
|
|
2317
|
-
if (manifest && !["exports", "main", "module", "bin"].some((key) => Boolean(manifest[key]))) return "application";
|
|
2318
|
-
} catch {
|
|
2319
|
-
}
|
|
2320
|
-
return "library";
|
|
2321
|
-
}
|
|
2322
|
-
|
|
2323
|
-
// src/generators/shared/frontend-app.ts
|
|
2324
|
-
var baseNameOf = (root) => root.split("/").pop() ?? root;
|
|
2325
|
-
function isEndToEnd(root) {
|
|
2326
|
-
const dir = baseNameOf(root);
|
|
2327
|
-
return dir === "e2e" || dir.endsWith("-e2e");
|
|
2328
|
-
}
|
|
2329
|
-
function isBackend(root, config, backendTag) {
|
|
2330
|
-
return baseNameOf(root) === "api" || (config.tags ?? []).includes(backendTag);
|
|
2331
|
-
}
|
|
2332
|
-
function prefixOf(root) {
|
|
2333
|
-
return root === "" || root === "." ? "" : `${root}/`;
|
|
2334
|
-
}
|
|
2335
|
-
function sourceRootOf(tree, config) {
|
|
2336
|
-
const prefix = prefixOf(config.root);
|
|
2337
|
-
const declared = config.sourceRoot?.replace(/\/$/, "");
|
|
2338
|
-
const atRoot = declared !== void 0 && (declared === config.root || prefix === "" && (declared === "." || declared === ""));
|
|
2339
|
-
if (declared && !atRoot) return declared;
|
|
2340
|
-
if (atRoot && !tree.exists(`${prefix}src`)) return prefix === "" ? "." : config.root;
|
|
2341
|
-
return `${prefix}src`;
|
|
2342
|
-
}
|
|
2343
|
-
function frameworkOf(tree, config) {
|
|
2344
|
-
return detectFramework({
|
|
2345
|
-
exists: (path) => tree.exists(path),
|
|
2346
|
-
read: (path) => tree.read(path, "utf-8"),
|
|
2347
|
-
projectRoot: config.root,
|
|
2348
|
-
executors: Object.values(config.targets ?? {}).flatMap((target) => target.executor ? [target.executor] : [])
|
|
2349
|
-
});
|
|
2350
|
-
}
|
|
2351
|
-
function resolveFrontendApp(tree, explicitProject) {
|
|
2352
|
-
let projects;
|
|
2353
|
-
try {
|
|
2354
|
-
projects = getProjects(tree);
|
|
2355
|
-
} catch (error) {
|
|
2356
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2357
|
-
const pathMatch = /Cannot parse ([^:]+):/.exec(message);
|
|
2358
|
-
return { kind: "invalid-project-json", detail: pathMatch ? pathMatch[1] : message };
|
|
2359
|
-
}
|
|
2360
|
-
const found = (project2, config2) => {
|
|
2361
|
-
const profile = frameworkOf(tree, config2);
|
|
2362
|
-
return { kind: "found", project: project2, root: config2.root, sourceRoot: sourceRootOf(tree, config2), framework: profile.id, profile };
|
|
2363
|
-
};
|
|
2364
|
-
if (explicitProject !== void 0) {
|
|
2365
|
-
const config2 = projects.get(explicitProject);
|
|
2366
|
-
if (config2) return found(explicitProject, config2);
|
|
2367
|
-
const rootApp = inferredRootApp(tree, projects);
|
|
2368
|
-
return rootApp?.kind === "found" && rootApp.project === explicitProject ? rootApp : { kind: "not-found", project: explicitProject };
|
|
2369
|
-
}
|
|
2370
|
-
const backendTag = readBoundaryTags(readToolkitConfig(tree)).backend;
|
|
2371
|
-
const eligible = [...projects.entries()].filter(([, config2]) => !isEndToEnd(config2.root) && !isBackend(config2.root, config2, backendTag));
|
|
2372
|
-
const declared = eligible.filter(([, config2]) => config2.projectType === "application");
|
|
2373
|
-
const inferred = eligible.filter(([, config2]) => {
|
|
2374
|
-
if (config2.projectType !== void 0) return false;
|
|
2375
|
-
if (config2.root === "" || config2.root === ".") return frameworkOf(tree, config2).recognised;
|
|
2376
|
-
return resolveProjectType(tree, config2.root, void 0) === "application";
|
|
2377
|
-
});
|
|
2378
|
-
const candidates = (declared.length > 0 ? declared : inferred).sort(([a], [b]) => a.localeCompare(b));
|
|
2379
|
-
if (candidates.length === 0) {
|
|
2380
|
-
const rootApp = inferredRootApp(tree, projects);
|
|
2381
|
-
if (rootApp !== null) return rootApp;
|
|
2382
|
-
const otherProjects = [...projects.keys()].filter((name) => !isEndToEnd(projects.get(name)?.root ?? "")).sort();
|
|
2383
|
-
return { kind: "none", otherProjects };
|
|
2384
|
-
}
|
|
2385
|
-
if (candidates.length > 1) return { kind: "ambiguous", candidates: candidates.map(([name]) => name) };
|
|
2386
|
-
const [project, config] = candidates[0];
|
|
2387
|
-
return found(project, config);
|
|
2388
|
-
}
|
|
2389
|
-
function inferredRootApp(tree, projects) {
|
|
2390
|
-
if ([...projects.values()].some((config2) => config2.root === "" || config2.root === ".")) return null;
|
|
2391
|
-
const raw = tree.read("package.json", "utf-8");
|
|
2392
|
-
if (raw === null) return null;
|
|
2393
|
-
let name;
|
|
2394
|
-
try {
|
|
2395
|
-
name = JSON.parse(raw).name;
|
|
2396
|
-
} catch {
|
|
2397
|
-
return null;
|
|
2398
|
-
}
|
|
2399
|
-
if (typeof name !== "string" || name === "") return null;
|
|
2400
|
-
const config = { name, root: "." };
|
|
2401
|
-
const framework = frameworkOf(tree, config);
|
|
2402
|
-
if (!framework.recognised) return null;
|
|
2403
|
-
return { kind: "found", project: name, root: ".", sourceRoot: sourceRootOf(tree, config), framework: framework.id, profile: framework, inferredRoot: true };
|
|
2404
|
-
}
|
|
2405
|
-
|
|
2406
|
-
// src/generators/add-fsd-lite/render.ts
|
|
2407
|
-
function renderAddFsdLiteReport(report) {
|
|
2408
|
-
const lines = [];
|
|
2409
|
-
if (report.aborted) {
|
|
2410
|
-
lines.push("add-fsd-lite aborted \u2014 nothing was written.");
|
|
2411
|
-
const refusal = report.appRefusal;
|
|
2412
|
-
if (refusal !== null) {
|
|
2413
|
-
lines.push("");
|
|
2414
|
-
lines.push(" FSD layers live inside the frontend app's source root (e.g. apps/web/src), and no app could be picked:");
|
|
2415
|
-
if (refusal.reason === "project-not-found") {
|
|
2416
|
-
lines.push(` --project=${refusal.project} names no project in this workspace.`);
|
|
2417
|
-
} else if (refusal.reason === "ambiguous-frontend") {
|
|
2418
|
-
lines.push(" more than one frontend app was found \u2014 rerun with --project=<one of them>:");
|
|
2419
|
-
for (const name of refusal.candidates) lines.push(` - ${name}`);
|
|
2420
|
-
} else if (refusal.reason === "invalid-project-json") {
|
|
2421
|
-
lines.push(` a project.json could not be parsed (${refusal.detail}) \u2014 fix it, then rerun.`);
|
|
2422
|
-
} else if (refusal.otherProjects.length > 0) {
|
|
2423
|
-
lines.push(" no project was recognised as a frontend app. If one of these is it, rerun with --project=<name>:");
|
|
2424
|
-
for (const name of refusal.otherProjects) lines.push(` - ${name}`);
|
|
2425
|
-
} else {
|
|
2426
|
-
lines.push(" the workspace has no projects \u2014 create one (e.g. nx g @nx/react:app or nx g @nx/next:app), then rerun.");
|
|
2427
|
-
}
|
|
2428
|
-
}
|
|
2429
|
-
return lines.join("\n");
|
|
2430
|
-
}
|
|
2431
|
-
if (report.dryRun) {
|
|
2432
|
-
lines.push("add-fsd-lite dry-run \u2014 no files were written.");
|
|
2433
|
-
lines.push("");
|
|
2434
|
-
}
|
|
2435
|
-
lines.push(`FSD layers (in ${report.project}, ${report.sourceRoot}):`);
|
|
2436
|
-
if (report.layerGuide !== null && report.frontend === "generic") {
|
|
2437
|
-
lines.push("Frontend: framework not recognised \u2014 the common FSD rules apply, with no framework-specific layer names.");
|
|
2438
|
-
}
|
|
2439
|
-
if (report.layerGuide !== null) lines.push(report.layerGuide);
|
|
2440
|
-
lines.push("");
|
|
2441
|
-
const where = report.eslintPath ?? `${report.project}'s eslint.config.mjs`;
|
|
2442
|
-
switch (report.eslint) {
|
|
2443
|
-
case "applied":
|
|
2444
|
-
lines.push(`ESLint: archweave/fsd-boundaries (${report.enforcement}) written to ${where}`);
|
|
2445
|
-
break;
|
|
2446
|
-
case "already-applied":
|
|
2447
|
-
lines.push(`ESLint: archweave/fsd-boundaries already wired in ${where} \u2014 no change`);
|
|
2448
|
-
break;
|
|
2449
|
-
case "degraded-unrecognized":
|
|
2450
|
-
lines.push(`ESLint: ${report.eslintPath === null ? `no ESLint config found for ${report.project}` : `${where} was NOT recognized \u2014 left untouched`}.`);
|
|
2451
|
-
lines.push(" Add the rule yourself (then the layer rules are actually enforced):");
|
|
2452
|
-
break;
|
|
2453
|
-
case "snippet-only":
|
|
2454
|
-
lines.push(`ESLint: report-only (pass --applyEslint to write the rule into ${where}).`);
|
|
2455
|
-
break;
|
|
2456
|
-
case "not-applied":
|
|
2457
|
-
break;
|
|
2458
|
-
}
|
|
2459
|
-
if (report.snippet !== null) lines.push(report.snippet);
|
|
2460
|
-
if (report.steiger !== null) {
|
|
2461
|
-
const st = report.steiger;
|
|
2462
|
-
lines.push("");
|
|
2463
|
-
lines.push("steiger (official FSD linter, opt-in):");
|
|
2464
|
-
lines.push(
|
|
2465
|
-
` ${st.configPath}: ${st.config === "created" ? "created" : st.config === "unchanged" ? "unchanged" : "kept \u2014 an existing config with other content is never replaced"}`
|
|
2466
|
-
);
|
|
2467
|
-
lines.push(
|
|
2468
|
-
st.target === "added" ? ` target ${"fsd-check"}: added to ${st.targetPath}` : st.target === "present" ? ` target ${"fsd-check"}: already present in ${st.targetPath}` : " target fsd-check: NOT added \u2014 no parseable project.json / package.json for the app"
|
|
2469
|
-
);
|
|
2470
|
-
lines.push(
|
|
2471
|
-
st.dependencies === "added" ? ` devDependencies: steiger + @feature-sliced/steiger-plugin added to package.json \u2014 run \`${st.installCommand}\`` : st.dependencies === "present" ? " devDependencies: already present" : " devDependencies: NOT added \u2014 the root package.json could not be parsed"
|
|
2472
|
-
);
|
|
2473
|
-
lines.push(` run it with: ${st.runCommand} (steiger needs Node >= 22.18)`);
|
|
2474
|
-
}
|
|
2475
|
-
return lines.join("\n");
|
|
2476
|
-
}
|
|
2477
|
-
|
|
2478
|
-
// src/generators/add-fsd-lite/generator.ts
|
|
2479
|
-
function resolveEnforcement(option, config) {
|
|
2480
|
-
if (option === "error" || option === "warn") return option;
|
|
2481
|
-
return readFsdEnforcement(config?.["fsd"]) ?? "warn";
|
|
2482
|
-
}
|
|
2483
|
-
function runAddFsdLite(tree, options) {
|
|
2484
|
-
const config = readToolkitConfig(tree);
|
|
2485
|
-
const locale = resolveLocaleOption(options.locale, config);
|
|
2486
|
-
const enforcement = resolveEnforcement(options.enforcement, config);
|
|
2487
|
-
const frontend = resolveFrontendOption(options.frontend, config, "add-fsd-lite");
|
|
2488
|
-
const dryRun = options.dryRun ?? false;
|
|
2489
|
-
const applyEslint = options.applyEslint ?? false;
|
|
2490
|
-
const app = resolveFrontendApp(tree, options.project);
|
|
2491
|
-
if (app.kind !== "found") {
|
|
2492
|
-
const snippet2 = buildFsdEslintPasteSnippet(enforcement, locale);
|
|
2493
|
-
const appRefusal = app.kind === "not-found" ? { reason: "project-not-found", project: app.project } : app.kind === "ambiguous" ? { reason: "ambiguous-frontend", candidates: app.candidates } : app.kind === "invalid-project-json" ? { reason: "invalid-project-json", detail: app.detail } : { reason: "no-frontend-app", otherProjects: app.otherProjects };
|
|
2494
|
-
return { dryRun, aborted: true, appRefusal, project: null, sourceRoot: null, enforcement, frontend, eslint: "not-applied", eslintPath: null, snippet: snippet2, layerGuide: null, steiger: null };
|
|
2495
|
-
}
|
|
2496
|
-
const appDir = app.root === "" || app.root === "." ? "" : `${app.root}/`;
|
|
2497
|
-
const eslintRoot = posix.relative(app.root === "" ? "." : app.root, app.sourceRoot) || ".";
|
|
2498
|
-
const profile = app.framework === frontend ? app.profile : profileById(frontend);
|
|
2499
|
-
const { platformSuffixes, routerDirs } = profile;
|
|
2500
|
-
const framework = { platformSuffixes, routerDirs };
|
|
2501
|
-
const snippet = buildFsdEslintPasteSnippet(enforcement, locale, eslintRoot, framework);
|
|
2502
|
-
const eslintPath = ESLINT_CONFIG_CANDIDATES.map((name) => `${appDir}${name}`).find((path) => tree.exists(path)) ?? null;
|
|
2503
|
-
const configText = eslintPath === null ? null : tree.read(eslintPath, "utf-8");
|
|
2504
|
-
let eslint;
|
|
2505
|
-
if (!applyEslint) {
|
|
2506
|
-
eslint = configText !== null && applyFsdEslintConfig(configText, enforcement, eslintRoot, framework).status === "already-applied" ? "already-applied" : "snippet-only";
|
|
2507
|
-
} else if (configText === null) {
|
|
2508
|
-
eslint = "degraded-unrecognized";
|
|
2509
|
-
} else {
|
|
2510
|
-
const result = applyFsdEslintConfig(configText, enforcement, eslintRoot, framework);
|
|
2511
|
-
eslint = result.status === "applied" ? "applied" : result.status === "already-applied" ? "already-applied" : "degraded-unrecognized";
|
|
2512
|
-
if (result.status === "applied" && !dryRun && eslintPath !== null) tree.write(eslintPath, result.text);
|
|
2513
|
-
}
|
|
2514
|
-
const steiger = options.steiger ? addSteiger(tree, { appDir, appRoot: app.root, sourceRoot: app.sourceRoot, profile, locale, write: !dryRun, config }) : null;
|
|
2515
|
-
return {
|
|
2516
|
-
dryRun,
|
|
2517
|
-
aborted: false,
|
|
2518
|
-
appRefusal: null,
|
|
2519
|
-
project: app.project,
|
|
2520
|
-
sourceRoot: app.sourceRoot,
|
|
2521
|
-
steiger,
|
|
2522
|
-
enforcement,
|
|
2523
|
-
frontend,
|
|
2524
|
-
eslint,
|
|
2525
|
-
eslintPath,
|
|
2526
|
-
snippet: eslint === "applied" || eslint === "already-applied" ? null : snippet,
|
|
2527
|
-
layerGuide: composeFsdLayerGuide({ profile, sourceRoot: app.sourceRoot, locale })
|
|
2528
|
-
};
|
|
2529
|
-
}
|
|
2530
|
-
function readJsonObject(tree, path) {
|
|
2531
|
-
const raw = tree.read(path, "utf-8");
|
|
2532
|
-
if (raw === null) return null;
|
|
2533
|
-
try {
|
|
2534
|
-
const parsed = JSON.parse(raw);
|
|
2535
|
-
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? { raw, parsed } : null;
|
|
2536
|
-
} catch {
|
|
2537
|
-
return null;
|
|
2538
|
-
}
|
|
2539
|
-
}
|
|
2540
|
-
function addSteiger(tree, input) {
|
|
2541
|
-
const configPath = `${input.appDir}${STEIGER_CONFIG_FILE}`;
|
|
2542
|
-
const configText = buildSteigerConfig({ renamedLayerDirs: Object.values(input.profile.layerDirs), locale: input.locale });
|
|
2543
|
-
const existing = tree.read(configPath, "utf-8");
|
|
2544
|
-
let configAction;
|
|
2545
|
-
if (existing === null) {
|
|
2546
|
-
configAction = "created";
|
|
2547
|
-
if (input.write) tree.write(configPath, configText);
|
|
2548
|
-
} else {
|
|
2549
|
-
configAction = existing === configText ? "unchanged" : "kept";
|
|
2550
|
-
}
|
|
2551
|
-
const rootPrefix = input.appRoot === "" || input.appRoot === "." ? "" : `${input.appRoot}/`;
|
|
2552
|
-
const sourceRootName = input.sourceRoot.startsWith(rootPrefix) ? input.sourceRoot.slice(rootPrefix.length) : "src";
|
|
2553
|
-
const target = buildFsdCheckTarget(sourceRootName);
|
|
2554
|
-
let targetAction = "unrecognized";
|
|
2555
|
-
let targetPath = null;
|
|
2556
|
-
for (const candidate of [`${input.appDir}project.json`, `${input.appDir}package.json`]) {
|
|
2557
|
-
const file = readJsonObject(tree, candidate);
|
|
2558
|
-
if (file === null) continue;
|
|
2559
|
-
targetPath = candidate;
|
|
2560
|
-
const holder = candidate.endsWith("project.json") ? file.parsed : file.parsed["nx"] ??= {};
|
|
2561
|
-
if (typeof holder !== "object" || holder === null) break;
|
|
2562
|
-
const targets = holder["targets"] ??= {};
|
|
2563
|
-
if (typeof targets !== "object" || targets === null) break;
|
|
2564
|
-
if (targets[FSD_CHECK_TARGET] !== void 0) {
|
|
2565
|
-
targetAction = "present";
|
|
2566
|
-
} else {
|
|
2567
|
-
targets[FSD_CHECK_TARGET] = target;
|
|
2568
|
-
targetAction = "added";
|
|
2569
|
-
const path = candidate.endsWith("project.json") ? ["targets", FSD_CHECK_TARGET] : ["nx", "targets", FSD_CHECK_TARGET];
|
|
2570
|
-
if (input.write) tree.write(candidate, setJsonValue(file.raw, path, target));
|
|
2571
|
-
}
|
|
2572
|
-
break;
|
|
2573
|
-
}
|
|
2574
|
-
let dependenciesAction = "unrecognized";
|
|
2575
|
-
const rootPackage = readJsonObject(tree, "package.json");
|
|
2576
|
-
if (rootPackage !== null) {
|
|
2577
|
-
const devDependencies = rootPackage.parsed["devDependencies"] ??= {};
|
|
2578
|
-
const dependencies = rootPackage.parsed["dependencies"] ?? {};
|
|
2579
|
-
const missing = Object.entries(STEIGER_DEPENDENCIES).filter(([name]) => devDependencies[name] === void 0 && dependencies[name] === void 0);
|
|
2580
|
-
if (missing.length === 0) {
|
|
2581
|
-
dependenciesAction = "present";
|
|
2582
|
-
} else {
|
|
2583
|
-
for (const [name, range] of missing) devDependencies[name] = range;
|
|
2584
|
-
dependenciesAction = "added";
|
|
2585
|
-
const edited = applyJsonEdits(rootPackage.raw, missing.map(([name, range]) => [["devDependencies", name], range]));
|
|
2586
|
-
if (input.write) tree.write("package.json", edited);
|
|
2587
|
-
}
|
|
2588
|
-
}
|
|
2589
|
-
const pm = treePackageManager(tree, input.config) ?? "pnpm";
|
|
2590
|
-
const commands = packageManagerCommands(pm, pm === "yarn" ? detectYarnFlavor((p) => tree.exists(p), (p) => tree.read(p, "utf-8")) : void 0);
|
|
2591
|
-
return {
|
|
2592
|
-
configPath,
|
|
2593
|
-
config: configAction,
|
|
2594
|
-
targetPath,
|
|
2595
|
-
target: targetAction,
|
|
2596
|
-
dependencies: dependenciesAction,
|
|
2597
|
-
installCommand: commands.install,
|
|
2598
|
-
runCommand: `${commands.runNx} run <project>:${FSD_CHECK_TARGET}`
|
|
2599
|
-
};
|
|
2600
|
-
}
|
|
2601
|
-
|
|
2602
|
-
// src/generators/add-agent-rules/render.ts
|
|
2603
|
-
var ACTION_LINE = {
|
|
2604
|
-
created: (result) => `${result.filePath}: created`,
|
|
2605
|
-
appended: (result) => `${result.filePath}: nx-archweave block added after the existing content (nothing else changed)`,
|
|
2606
|
-
updated: (result) => `${result.filePath}: nx-archweave block updated (nothing outside it changed)`,
|
|
2607
|
-
unchanged: (result) => `${result.filePath}: unchanged`,
|
|
2608
|
-
unrecognized: (result) => `${result.filePath}: not written \u2014 ${result.reason ?? "the nx-archweave markers could not be placed"}`,
|
|
2609
|
-
"user-modified": (result) => `${result.filePath}: not written \u2014 the nx-archweave block was edited by hand, so it is kept`
|
|
2610
|
-
};
|
|
2611
|
-
function renderAddAgentRulesReport(report) {
|
|
2612
|
-
const lines = [];
|
|
2613
|
-
lines.push(report.dryRun ? "add-agent-rules dry-run \u2014 no files were written." : "add-agent-rules:");
|
|
2614
|
-
lines.push(` hosts: ${report.hosts.join(", ")}`);
|
|
2615
|
-
lines.push(` locale: ${report.locale}`);
|
|
2616
|
-
lines.push(` teamRules: ${report.teamRules}`);
|
|
2617
|
-
lines.push("");
|
|
2618
|
-
for (const file of report.files) {
|
|
2619
|
-
lines.push(` ${ACTION_LINE[file.action](file)}`);
|
|
2620
|
-
if (file.action === "user-modified") {
|
|
2621
|
-
lines.push(" What this run would write instead (your block \u2192 generated):");
|
|
2622
|
-
for (const line of (file.diff ?? "").split("\n")) lines.push(` ${line}`);
|
|
2623
|
-
lines.push(" Rerun with --overwriteBlock to replace the block with the generated one.");
|
|
2624
|
-
}
|
|
2625
|
-
}
|
|
2626
|
-
const RECORD_LINE = {
|
|
2627
|
-
created: "toolkit.config.json: aiHosts recorded (file created)",
|
|
2628
|
-
updated: "toolkit.config.json: aiHosts recorded",
|
|
2629
|
-
unchanged: "toolkit.config.json: aiHosts already recorded",
|
|
2630
|
-
unrecognized: "toolkit.config.json: not a JSON object \u2014 aiHosts not recorded (fix the file, then rerun)",
|
|
2631
|
-
"not-applied": "toolkit.config.json: aiHosts not recorded \u2014 a file above was not written",
|
|
2632
|
-
"not-requested": null
|
|
2633
|
-
};
|
|
2634
|
-
const recordLine = RECORD_LINE[report.hostsRecord];
|
|
2635
|
-
if (recordLine !== null) lines.push(` ${recordLine}`);
|
|
2636
|
-
if (report.files.some((file) => file.action === "unrecognized")) {
|
|
2637
|
-
lines.push("");
|
|
2638
|
-
lines.push('Manual fix needed: keep exactly one "<!-- nx-archweave:agent-rules start \u2026 -->" \u2026 "<!-- nx-archweave:agent-rules end -->"');
|
|
2639
|
-
lines.push("pair in the file (or remove both markers), then rerun add-agent-rules.");
|
|
2640
|
-
}
|
|
2641
|
-
if (report.dryRun) {
|
|
2642
|
-
lines.push("");
|
|
2643
|
-
lines.push("Generated content preview:");
|
|
2644
|
-
for (const file of report.files) {
|
|
2645
|
-
if (file.content === null) continue;
|
|
2646
|
-
lines.push("");
|
|
2647
|
-
lines.push(`--- ${file.filePath} ---`);
|
|
2648
|
-
lines.push(file.content);
|
|
2649
|
-
}
|
|
2650
|
-
}
|
|
2651
|
-
return lines.join("\n");
|
|
2652
|
-
}
|
|
2653
|
-
|
|
2654
|
-
// src/generators/add-review/render.ts
|
|
2655
|
-
var DEPENDABOT_PATH = ".github/dependabot.yml";
|
|
2656
|
-
var PROVIDER_STATUS_URL = "https://github.com/clearlydev/nx-archweave/blob/main/docs/pr-review.md#provider-status";
|
|
2657
|
-
function renderLineDiff(existing, generated) {
|
|
2658
|
-
const a = existing.split("\n");
|
|
2659
|
-
const b = generated.split("\n");
|
|
2660
|
-
const m = a.length;
|
|
2661
|
-
const n = b.length;
|
|
2662
|
-
const lcs = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
|
|
2663
|
-
for (let i2 = m - 1; i2 >= 0; i2--) {
|
|
2664
|
-
for (let j2 = n - 1; j2 >= 0; j2--) {
|
|
2665
|
-
lcs[i2][j2] = a[i2] === b[j2] ? lcs[i2 + 1][j2 + 1] + 1 : Math.max(lcs[i2 + 1][j2], lcs[i2][j2 + 1]);
|
|
2666
|
-
}
|
|
2667
|
-
}
|
|
2668
|
-
const out = ["--- existing", "+++ generated"];
|
|
2669
|
-
let i = 0;
|
|
2670
|
-
let j = 0;
|
|
2671
|
-
while (i < m && j < n) {
|
|
2672
|
-
if (a[i] === b[j]) {
|
|
2673
|
-
out.push(` ${a[i]}`);
|
|
2674
|
-
i++;
|
|
2675
|
-
j++;
|
|
2676
|
-
} else if (lcs[i + 1][j] >= lcs[i][j + 1]) {
|
|
2677
|
-
out.push(`- ${a[i]}`);
|
|
2678
|
-
i++;
|
|
2679
|
-
} else {
|
|
2680
|
-
out.push(`+ ${b[j]}`);
|
|
2681
|
-
j++;
|
|
2682
|
-
}
|
|
2683
|
-
}
|
|
2684
|
-
while (i < m) out.push(`- ${a[i++]}`);
|
|
2685
|
-
while (j < n) out.push(`+ ${b[j++]}`);
|
|
2686
|
-
return out.join("\n");
|
|
2687
|
-
}
|
|
2688
|
-
var WORKFLOW_ACTION_LINES = {
|
|
2689
|
-
created: "created",
|
|
2690
|
-
overwritten: "overwritten with the generated version (per --onConflict=overwrite)",
|
|
2691
|
-
unchanged: "unchanged (already matches the generated version)",
|
|
2692
|
-
proposed: "NOT written \u2014 differs from the generated version (merge proposal below)",
|
|
2693
|
-
skipped: "left as-is (per --onConflict=skip)",
|
|
2694
|
-
alternate: "generated alongside the existing file",
|
|
2695
|
-
aborted: "NOT written"
|
|
2696
|
-
};
|
|
2697
|
-
var CONFIG_ACTION_LINES = {
|
|
2698
|
-
created: "created with the review field",
|
|
2699
|
-
updated: "review field updated",
|
|
2700
|
-
unchanged: "review field unchanged",
|
|
2701
|
-
"not-updated": "NOT updated \u2014 kept consistent with the workflow file that is actually in effect",
|
|
2702
|
-
aborted: "NOT updated"
|
|
2703
|
-
};
|
|
2704
|
-
function renderAddReviewReport(report) {
|
|
2705
|
-
const lines = [];
|
|
2706
|
-
if (report.packageManagerNote !== void 0) {
|
|
2707
|
-
lines.push(`Note: ${report.packageManagerNote}`);
|
|
2708
|
-
}
|
|
2709
|
-
if (report.workflowAction === "aborted") {
|
|
2710
|
-
lines.push("add-review aborted \u2014 nothing was written.");
|
|
2711
|
-
lines.push(` toolkit.config.json: ${report.configError ?? "unusable"}`);
|
|
2712
|
-
lines.push(" Fix the config file manually, then rerun the generator.");
|
|
2713
|
-
return lines.join("\n");
|
|
2714
|
-
}
|
|
2715
|
-
if (report.dryRun) {
|
|
2716
|
-
lines.push("add-review dry-run \u2014 no files were written.");
|
|
2717
|
-
lines.push("");
|
|
2718
|
-
lines.push("Generated workflow YAML:");
|
|
2719
|
-
lines.push(report.workflowYaml ?? "");
|
|
2720
|
-
lines.push("");
|
|
2721
|
-
lines.push("toolkit.config.json review field:");
|
|
2722
|
-
lines.push(report.configSnippetJson ?? "");
|
|
2723
|
-
lines.push("");
|
|
2724
|
-
lines.push("Effective decision:");
|
|
2725
|
-
}
|
|
2726
|
-
lines.push(` ${report.workflowPath}: ${WORKFLOW_ACTION_LINES[report.workflowAction]}`);
|
|
2727
|
-
lines.push(` toolkit.config.json: ${CONFIG_ACTION_LINES[report.configAction]}`);
|
|
2728
|
-
if (report.workflowAction === "proposed" && report.diff) {
|
|
2729
|
-
lines.push("");
|
|
2730
|
-
lines.push(report.diff);
|
|
2731
|
-
lines.push("");
|
|
2732
|
-
lines.push("To resolve, rerun with one of:");
|
|
2733
|
-
lines.push(" --onConflict=overwrite replace the existing file with the generated version");
|
|
2734
|
-
lines.push(" --onConflict=skip keep the existing file");
|
|
2735
|
-
lines.push(" --onConflict=alternate-filename write the generated version alongside it");
|
|
2736
|
-
}
|
|
2737
|
-
if (report.dependabot === "created") {
|
|
2738
|
-
lines.push(` ${DEPENDABOT_PATH}: created \u2014 Dependabot proposes newer pins for the workflow's actions`);
|
|
2739
|
-
} else if (report.dependabot === "present") {
|
|
2740
|
-
lines.push(` ${DEPENDABOT_PATH}: already updates github-actions`);
|
|
2741
|
-
} else if (report.dependabot === "proposed") {
|
|
2742
|
-
lines.push(` ${DEPENDABOT_PATH}: NOT changed \u2014 it has no github-actions entry. The workflow pins each action`);
|
|
2743
|
-
lines.push(" to a commit SHA; add this entry under `updates:` so those pins get update pull requests:");
|
|
2744
|
-
lines.push("");
|
|
2745
|
-
lines.push(report.dependabotProposal ?? "");
|
|
2746
|
-
}
|
|
2747
|
-
if (report.workflowAction === "alternate") {
|
|
2748
|
-
lines.push("");
|
|
2749
|
-
lines.push(" Review the generated file, then replace the main workflow file with it yourself;");
|
|
2750
|
-
lines.push(" the config stays pointing at the workflow that is actually in effect until you do.");
|
|
2751
|
-
}
|
|
2752
|
-
lines.push("");
|
|
2753
|
-
lines.push(`Note: the review workflow stays green-skipped until you register the ${report.requiredSecretName} secret.`);
|
|
2754
|
-
if (report.betaProvider !== void 0) {
|
|
2755
|
-
lines.push(`Note: the ${report.betaProvider} provider is in beta \u2014 its workflow hasn't finished a review end to end on`);
|
|
2756
|
-
lines.push(`GitHub Actions yet. Status, costs and known issues: ${PROVIDER_STATUS_URL}`);
|
|
2757
|
-
}
|
|
2758
|
-
lines.push("Note: this workflow runs with elevated permissions only after it is merged to the default branch \u2014");
|
|
2759
|
-
lines.push("merging is the first real validation.");
|
|
2760
|
-
return lines.join("\n");
|
|
2761
|
-
}
|
|
2762
|
-
|
|
2763
|
-
// src/eslint/fsd-policy.ts
|
|
2764
|
-
var PRESETS = {
|
|
2765
|
-
basic: {
|
|
2766
|
-
sliceGroups: { auto: true, include: [], exclude: [] },
|
|
2767
|
-
sharedPublicApi: "segment",
|
|
2768
|
-
testFiles: false,
|
|
2769
|
-
segments: [],
|
|
2770
|
-
platformSuffixes: [],
|
|
2771
|
-
routerDirs: []
|
|
2772
|
-
},
|
|
2773
|
-
practical: {
|
|
2774
|
-
sliceGroups: { auto: true, include: [], exclude: [] },
|
|
2775
|
-
sharedPublicApi: "file",
|
|
2776
|
-
testFiles: ["**/*.{spec,test}.*", "**/__tests__/**"],
|
|
2777
|
-
segments: [],
|
|
2778
|
-
platformSuffixes: [],
|
|
2779
|
-
routerDirs: []
|
|
2780
|
-
}
|
|
2781
|
-
};
|
|
2782
|
-
var isStringArray = (value) => Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
2783
|
-
function resolveFsdPolicy(recorded, overrides = {}) {
|
|
2784
|
-
const fsd = typeof recorded === "object" && recorded !== null ? recorded : {};
|
|
2785
|
-
const preset = fsd["preset"] === "practical" ? "practical" : "basic";
|
|
2786
|
-
const policy = { ...PRESETS[preset] };
|
|
2787
|
-
const groups = fsd["sliceGroups"];
|
|
2788
|
-
if (groups === false) policy.sliceGroups = false;
|
|
2789
|
-
else if (groups === "auto") policy.sliceGroups = { auto: true, include: [], exclude: [] };
|
|
2790
|
-
else if (typeof groups === "object" && groups !== null) {
|
|
2791
|
-
const value = groups;
|
|
2792
|
-
policy.sliceGroups = {
|
|
2793
|
-
auto: value["auto"] !== false,
|
|
2794
|
-
include: isStringArray(value["include"]) ? value["include"] : [],
|
|
2795
|
-
exclude: isStringArray(value["exclude"]) ? value["exclude"] : []
|
|
2796
|
-
};
|
|
2797
|
-
}
|
|
2798
|
-
const shared = fsd["sharedPublicApi"];
|
|
2799
|
-
if (shared === "segment" || shared === "component" || shared === "file") policy.sharedPublicApi = shared;
|
|
2800
|
-
const tests = fsd["testFiles"];
|
|
2801
|
-
if (tests === false || isStringArray(tests)) policy.testFiles = tests;
|
|
2802
|
-
if (isStringArray(fsd["segments"])) policy.segments = fsd["segments"];
|
|
2803
|
-
if (isStringArray(fsd["platformSuffixes"])) policy.platformSuffixes = fsd["platformSuffixes"];
|
|
2804
|
-
if (isStringArray(fsd["routerDirs"])) policy.routerDirs = fsd["routerDirs"];
|
|
2805
|
-
for (const [key, value] of Object.entries(overrides)) {
|
|
2806
|
-
if (value !== void 0) policy[key] = value;
|
|
2807
|
-
}
|
|
2808
|
-
return policy;
|
|
2809
|
-
}
|
|
2810
|
-
|
|
2811
|
-
// src/generators/add-agent-rules/generator.ts
|
|
2812
|
-
function resolveTeamRules(option, config) {
|
|
2813
|
-
if (option !== void 0) return option;
|
|
2814
|
-
const agentRules = config?.["agentRules"];
|
|
2815
|
-
const teamRules = typeof agentRules === "object" && agentRules !== null ? agentRules["teamRules"] : void 0;
|
|
2816
|
-
return typeof teamRules === "boolean" ? teamRules : false;
|
|
2817
|
-
}
|
|
2818
|
-
function resolveFsdStructure(config) {
|
|
2819
|
-
const fsd = config?.["fsd"];
|
|
2820
|
-
const enabled = typeof fsd === "object" && fsd !== null ? fsd["enabled"] : void 0;
|
|
2821
|
-
if (typeof enabled === "boolean") return enabled;
|
|
2822
|
-
const frontend = config?.["frontend"];
|
|
2823
|
-
return typeof frontend === "string" && frontend !== "none";
|
|
2824
|
-
}
|
|
2825
|
-
function resolveModuleBoundaries(config) {
|
|
2826
|
-
const backend = config?.["backend"];
|
|
2827
|
-
return backend === "express" || backend === "nest" ? readBoundaryTags(config) : void 0;
|
|
2828
|
-
}
|
|
2829
|
-
var POLICY_KEYS = ["preset", "sliceGroups", "sharedPublicApi", "testFiles"];
|
|
2830
|
-
function resolveFsdPolicyNotes(config) {
|
|
2831
|
-
const fsd = config?.["fsd"];
|
|
2832
|
-
if (typeof fsd !== "object" || fsd === null || !POLICY_KEYS.some((key) => key in fsd)) return void 0;
|
|
2833
|
-
const policy = resolveFsdPolicy(fsd);
|
|
2834
|
-
return { sliceGroups: policy.sliceGroups !== false, sharedPublicApi: policy.sharedPublicApi, testFiles: policy.testFiles !== false };
|
|
2835
|
-
}
|
|
2836
|
-
function runAddAgentRules(tree, options, { recordHosts = true, fsdStructure, moduleBoundaries } = {}) {
|
|
2837
|
-
const config = readToolkitConfig(tree);
|
|
2838
|
-
const hosts = resolveAiHostsOption(options.hosts, config);
|
|
2839
|
-
const files = rulesFilesFor(hosts);
|
|
2840
|
-
const locale = resolveLocaleOption(options.locale, config);
|
|
2841
|
-
const teamRules = resolveTeamRules(options.teamRules, config);
|
|
2842
|
-
const dryRun = options.dryRun ?? false;
|
|
2843
|
-
const boundaries = moduleBoundaries === void 0 ? resolveModuleBoundaries(config) : moduleBoundaries ?? void 0;
|
|
2844
|
-
const composed = composeAgentRules({
|
|
2845
|
-
files,
|
|
2846
|
-
locale,
|
|
2847
|
-
teamRules,
|
|
2848
|
-
fsdStructure: fsdStructure ?? resolveFsdStructure(config),
|
|
2849
|
-
fsdPolicy: resolveFsdPolicyNotes(config),
|
|
2850
|
-
...boundaries !== void 0 ? { moduleBoundaries: boundaries } : {}
|
|
2851
|
-
});
|
|
2852
|
-
const results = composed.map(({ filePath, content }) => {
|
|
2853
|
-
if (!validateAgentRules(content, teamRules ? ["team-rules"] : [])) {
|
|
2854
|
-
throw new Error(
|
|
2855
|
-
`Generated ${filePath} is missing a required section (Safety Rules, Language Policy${teamRules ? ", Team Rules" : ""})`
|
|
2856
|
-
);
|
|
2857
|
-
}
|
|
2858
|
-
const existing = tree.read(filePath, "utf-8");
|
|
2859
|
-
const outcome = applyAgentRulesBlock(existing, content, { overwrite: options.overwriteBlock === true });
|
|
2860
|
-
switch (outcome.kind) {
|
|
2861
|
-
case "unchanged":
|
|
2862
|
-
return { filePath, action: "unchanged", reason: null, content: null };
|
|
2863
|
-
case "unrecognized":
|
|
2864
|
-
return { filePath, action: "unrecognized", reason: outcome.reason, content: null };
|
|
2865
|
-
case "user-modified":
|
|
2866
|
-
return {
|
|
2867
|
-
filePath,
|
|
2868
|
-
action: "user-modified",
|
|
2869
|
-
reason: null,
|
|
2870
|
-
content: null,
|
|
2871
|
-
diff: renderLineDiff(outcome.existingBody, outcome.generatedBody)
|
|
2872
|
-
};
|
|
2873
|
-
case "create":
|
|
2874
|
-
case "append":
|
|
2875
|
-
case "replace": {
|
|
2876
|
-
if (!dryRun) tree.write(filePath, outcome.text);
|
|
2877
|
-
const action = outcome.kind === "create" ? "created" : outcome.kind === "append" ? "appended" : "updated";
|
|
2878
|
-
return { filePath, action, reason: null, content: dryRun ? outcome.text : null };
|
|
2879
|
-
}
|
|
2880
|
-
}
|
|
2881
|
-
});
|
|
2882
|
-
const explicit = options.hosts !== void 0 && options.hosts.length > 0;
|
|
2883
|
-
const allApplied = results.every((file) => file.action !== "unrecognized");
|
|
2884
|
-
const hostsRecord = explicit && recordHosts ? allApplied ? recordAiHosts(tree, hosts, !dryRun) : "not-applied" : "not-requested";
|
|
2885
|
-
return { dryRun, locale, hosts, teamRules, files: results, hostsRecord };
|
|
2886
|
-
}
|
|
2887
|
-
|
|
2888
|
-
// src/generators/add-github-templates/generator.ts
|
|
2889
|
-
import { join } from "node:path";
|
|
2890
|
-
|
|
2891
|
-
// src/generators/shared/template-files.ts
|
|
2892
|
-
import { existsSync } from "node:fs";
|
|
2893
|
-
import { fileURLToPath } from "node:url";
|
|
2894
|
-
import { generateFiles } from "@nx/devkit";
|
|
2895
|
-
import { createTree } from "@nx/devkit/testing";
|
|
2896
|
-
function generatorFilesDir(generator, fromUrl) {
|
|
2897
|
-
for (const relative of [`../${generator}/files`, `./generators/${generator}/files`]) {
|
|
2898
|
-
const dir = fileURLToPath(new URL(relative, fromUrl));
|
|
2899
|
-
if (existsSync(dir)) return dir;
|
|
2900
|
-
}
|
|
2901
|
-
throw new Error(`The "${generator}" generator's template files are missing next to ${fileURLToPath(fromUrl)}`);
|
|
2902
|
-
}
|
|
2903
|
-
function renderTemplateFiles(srcFolder, target, substitutions) {
|
|
2904
|
-
const scratch = createTree();
|
|
2905
|
-
const seeded = new Set(scratch.listChanges().map((change) => change.path));
|
|
2906
|
-
generateFiles(scratch, srcFolder, target, substitutions);
|
|
2907
|
-
return scratch.listChanges().flatMap(
|
|
2908
|
-
(change) => change.type === "CREATE" && change.content !== null && !seeded.has(change.path) ? [{ filePath: change.path, content: change.content.toString("utf-8") }] : []
|
|
2909
|
-
).sort((a, b) => a.filePath.localeCompare(b.filePath));
|
|
2910
|
-
}
|
|
2911
|
-
|
|
2912
|
-
// src/generators/add-github-templates/render.ts
|
|
2913
|
-
var ACTION_LINES = {
|
|
2914
|
-
created: "created",
|
|
2915
|
-
unchanged: "unchanged (already matches the generated version)",
|
|
2916
|
-
skipped: "left as-is (a different file already exists there \u2014 never overwritten)"
|
|
2917
|
-
};
|
|
2918
|
-
function renderAddGithubTemplatesReport(report) {
|
|
2919
|
-
const lines = [];
|
|
2920
|
-
if (report.files.length === 0) {
|
|
2921
|
-
lines.push("add-github-templates: nothing selected (issue/pr/codeowners all false) \u2014 no files to install.");
|
|
2922
|
-
return lines.join("\n");
|
|
2923
|
-
}
|
|
2924
|
-
if (report.dryRun) {
|
|
2925
|
-
lines.push("add-github-templates dry-run \u2014 no files were written.");
|
|
2926
|
-
lines.push("");
|
|
2927
|
-
for (const file of report.files) {
|
|
2928
|
-
lines.push(`${file.filePath}:`);
|
|
2929
|
-
lines.push(file.content);
|
|
2930
|
-
lines.push("");
|
|
2931
|
-
}
|
|
2932
|
-
lines.push("Effective decision:");
|
|
2933
|
-
}
|
|
2934
|
-
for (const file of report.files) {
|
|
2935
|
-
lines.push(` ${file.filePath}: ${ACTION_LINES[file.action]}`);
|
|
2936
|
-
}
|
|
2937
|
-
return lines.join("\n");
|
|
2938
|
-
}
|
|
2939
|
-
|
|
2940
|
-
// src/generators/add-github-templates/generator.ts
|
|
2941
|
-
function composeGithubTemplates(groups, locale) {
|
|
2942
|
-
const files = generatorFilesDir("add-github-templates", import.meta.url);
|
|
2943
|
-
return ["issue", "pr", "codeowners"].filter((group) => groups[group]).flatMap((group) => renderTemplateFiles(join(files, locale, group), ".", {}));
|
|
2944
|
-
}
|
|
2945
|
-
function runAddGithubTemplates(tree, options) {
|
|
2946
|
-
const locale = resolveLocaleOption(options.locale, readToolkitConfig(tree));
|
|
2947
|
-
const plan = composeGithubTemplates(
|
|
2948
|
-
{ issue: options.issue ?? false, pr: options.pr ?? true, codeowners: options.codeowners ?? false },
|
|
2949
|
-
locale
|
|
2950
|
-
);
|
|
2951
|
-
const dryRun = options.dryRun ?? false;
|
|
2952
|
-
const files = plan.map(({ filePath, content }) => {
|
|
2953
|
-
const existing = tree.read(filePath, "utf-8");
|
|
2954
|
-
let action;
|
|
2955
|
-
if (existing === null) {
|
|
2956
|
-
action = "created";
|
|
2957
|
-
if (!dryRun) tree.write(filePath, content);
|
|
2958
|
-
} else if (existing === content) {
|
|
2959
|
-
action = "unchanged";
|
|
2960
|
-
} else {
|
|
2961
|
-
action = "skipped";
|
|
2962
|
-
}
|
|
2963
|
-
return { filePath, action, content };
|
|
2964
|
-
});
|
|
2965
|
-
return { dryRun, files };
|
|
2966
|
-
}
|
|
2967
|
-
|
|
2968
|
-
// src/generators/add-review/generator.ts
|
|
2969
|
-
var CONFIG_PATH = "toolkit.config.json";
|
|
2970
|
-
function deriveAltWorkflowPath(mainWorkflowPath) {
|
|
2971
|
-
return mainWorkflowPath.replace(/\.yml$/, ".archweave.yml");
|
|
2972
|
-
}
|
|
2973
|
-
function readConfig(tree) {
|
|
2974
|
-
const raw = tree.read(CONFIG_PATH, "utf-8");
|
|
2975
|
-
if (raw === null) return { raw: null, parsed: null, error: null };
|
|
2976
|
-
try {
|
|
2977
|
-
const parsed = JSON.parse(raw);
|
|
2978
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
2979
|
-
return { raw, parsed: null, error: `${CONFIG_PATH} must contain a top-level JSON object` };
|
|
2980
|
-
}
|
|
2981
|
-
return { raw, parsed, error: null };
|
|
2982
|
-
} catch {
|
|
2983
|
-
return { raw, parsed: null, error: `${CONFIG_PATH} is not valid JSON` };
|
|
2984
|
-
}
|
|
2985
|
-
}
|
|
2986
|
-
function resolveLocale(option, config) {
|
|
2987
|
-
if (option) return option;
|
|
2988
|
-
const fromConfig = config.parsed?.["locale"];
|
|
2989
|
-
return fromConfig === "ko" || fromConfig === "en" ? fromConfig : "en";
|
|
2990
|
-
}
|
|
2991
|
-
function deepEqual(a, b) {
|
|
2992
|
-
if (a === b) return true;
|
|
2993
|
-
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
|
|
2994
|
-
if (Array.isArray(a) || Array.isArray(b)) {
|
|
2995
|
-
return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((value, index) => deepEqual(value, b[index]));
|
|
2996
|
-
}
|
|
2997
|
-
const aKeys = Object.keys(a).sort();
|
|
2998
|
-
const bKeys = Object.keys(b).sort();
|
|
2999
|
-
if (aKeys.length !== bKeys.length || aKeys.some((key, index) => key !== bKeys[index])) return false;
|
|
3000
|
-
return aKeys.every((key) => deepEqual(a[key], b[key]));
|
|
3001
|
-
}
|
|
3002
|
-
var PROVIDERS = ["claude", "gemini", "gpt", "cursor"];
|
|
3003
|
-
var BETA_PROVIDERS = ["gemini", "gpt", "cursor"];
|
|
3004
|
-
var MODES = ["fast", "standard", "thorough"];
|
|
3005
|
-
function recordedReview(config) {
|
|
3006
|
-
const review = config?.["review"];
|
|
3007
|
-
if (typeof review !== "object" || review === null || Array.isArray(review)) return {};
|
|
3008
|
-
const record = review;
|
|
3009
|
-
const pick = (value, allowed) => typeof value === "string" && allowed.includes(value) ? value : void 0;
|
|
3010
|
-
const rounds = record["maxRounds"];
|
|
3011
|
-
return {
|
|
3012
|
-
provider: pick(record["provider"], PROVIDERS),
|
|
3013
|
-
mode: pick(record["mode"], MODES),
|
|
3014
|
-
maxRounds: rounds === 1 || rounds === 2 || rounds === 3 || rounds === 4 || rounds === 5 ? rounds : void 0
|
|
3015
|
-
};
|
|
3016
|
-
}
|
|
3017
|
-
function runAddReview(tree, options) {
|
|
3018
|
-
const config = readConfig(tree);
|
|
3019
|
-
const detectedPm = treePackageManager(tree, config.parsed);
|
|
3020
|
-
const packageManager = options.packageManager ?? detectedPm ?? "pnpm";
|
|
3021
|
-
const packageManagerNote = options.packageManager === void 0 && detectedPm === null ? "the package manager could not be detected (no pnpm, yarn, bun or npm lockfile) \u2014 the workflow was rendered for pnpm; re-run with --packageManager=npm|yarn|bun if that is wrong." : void 0;
|
|
3022
|
-
const recorded = recordedReview(config.parsed);
|
|
3023
|
-
const provider = options.provider ?? recorded.provider ?? "claude";
|
|
3024
|
-
const plan = composeReviewSetup({
|
|
3025
|
-
enabled: true,
|
|
3026
|
-
provider,
|
|
3027
|
-
mode: options.mode ?? recorded.mode ?? "standard",
|
|
3028
|
-
locale: resolveLocale(options.locale, config),
|
|
3029
|
-
maxRounds: options.maxRounds ?? recorded.maxRounds ?? 3,
|
|
3030
|
-
packageManager
|
|
3031
|
-
});
|
|
3032
|
-
if (plan === null) throw new Error("unreachable: add-review always composes with enabled: true");
|
|
3033
|
-
if (config.error !== null) {
|
|
3034
|
-
return {
|
|
3035
|
-
dryRun: options.dryRun ?? false,
|
|
3036
|
-
workflowPath: plan.workflowFilePath,
|
|
3037
|
-
workflowAction: "aborted",
|
|
3038
|
-
configAction: "aborted",
|
|
3039
|
-
requiredSecretName: plan.requiredSecretName,
|
|
3040
|
-
diff: null,
|
|
3041
|
-
workflowYaml: null,
|
|
3042
|
-
configSnippetJson: null,
|
|
3043
|
-
configError: config.error,
|
|
3044
|
-
dependabot: "not-applied",
|
|
3045
|
-
dependabotProposal: null
|
|
3046
|
-
};
|
|
3047
|
-
}
|
|
3048
|
-
const existing = tree.read(plan.workflowFilePath, "utf-8");
|
|
3049
|
-
const verdict = judgeReviewFileConflict(existing, plan.workflowYaml);
|
|
3050
|
-
let workflowAction;
|
|
3051
|
-
let workflowPath = plan.workflowFilePath;
|
|
3052
|
-
let writeWorkflowTo = null;
|
|
3053
|
-
let diff = null;
|
|
3054
|
-
let mainFileMatchesOptions;
|
|
3055
|
-
if (verdict.kind === "create") {
|
|
3056
|
-
workflowAction = "created";
|
|
3057
|
-
writeWorkflowTo = plan.workflowFilePath;
|
|
3058
|
-
mainFileMatchesOptions = true;
|
|
3059
|
-
} else if (verdict.kind === "unchanged") {
|
|
3060
|
-
workflowAction = "unchanged";
|
|
3061
|
-
mainFileMatchesOptions = true;
|
|
3062
|
-
} else {
|
|
3063
|
-
switch (options.onConflict) {
|
|
3064
|
-
case "overwrite":
|
|
3065
|
-
workflowAction = "overwritten";
|
|
3066
|
-
writeWorkflowTo = plan.workflowFilePath;
|
|
3067
|
-
mainFileMatchesOptions = true;
|
|
3068
|
-
break;
|
|
3069
|
-
case "skip":
|
|
3070
|
-
workflowAction = "skipped";
|
|
3071
|
-
mainFileMatchesOptions = false;
|
|
3072
|
-
break;
|
|
3073
|
-
case "alternate-filename": {
|
|
3074
|
-
const altWorkflowPath = deriveAltWorkflowPath(plan.workflowFilePath);
|
|
3075
|
-
workflowAction = "alternate";
|
|
3076
|
-
workflowPath = altWorkflowPath;
|
|
3077
|
-
writeWorkflowTo = altWorkflowPath;
|
|
3078
|
-
mainFileMatchesOptions = false;
|
|
3079
|
-
break;
|
|
3080
|
-
}
|
|
3081
|
-
default:
|
|
3082
|
-
workflowAction = "proposed";
|
|
3083
|
-
diff = renderLineDiff(verdict.existing, verdict.generated);
|
|
3084
|
-
mainFileMatchesOptions = false;
|
|
3085
|
-
}
|
|
3086
|
-
}
|
|
3087
|
-
let configAction;
|
|
3088
|
-
let newConfigContent = null;
|
|
3089
|
-
if (!mainFileMatchesOptions) {
|
|
3090
|
-
configAction = "not-updated";
|
|
3091
|
-
} else {
|
|
3092
|
-
const existingReview = config.parsed?.["review"];
|
|
3093
|
-
if (deepEqual(existingReview, plan.configSnippet)) {
|
|
3094
|
-
configAction = "unchanged";
|
|
3095
|
-
} else {
|
|
3096
|
-
configAction = config.raw === null ? "created" : "updated";
|
|
3097
|
-
newConfigContent = applyJsonEdits(config.raw, [[["review"], plan.configSnippet]]);
|
|
3098
|
-
}
|
|
3099
|
-
}
|
|
3100
|
-
const [dependabotFile] = renderTemplateFiles(generatorFilesDir("add-review", import.meta.url), ".github", {});
|
|
3101
|
-
const existingDependabot = tree.read(DEPENDABOT_PATH, "utf-8");
|
|
3102
|
-
const dependabot = existingDependabot === null ? "created" : /github-actions/.test(existingDependabot) ? "present" : "proposed";
|
|
3103
|
-
const dryRun = options.dryRun ?? false;
|
|
3104
|
-
if (!dryRun) {
|
|
3105
|
-
if (writeWorkflowTo !== null) tree.write(writeWorkflowTo, plan.workflowYaml);
|
|
3106
|
-
if (newConfigContent !== null) tree.write(CONFIG_PATH, newConfigContent);
|
|
3107
|
-
if (dependabot === "created") tree.write(DEPENDABOT_PATH, dependabotFile.content);
|
|
3108
|
-
}
|
|
3109
|
-
return {
|
|
3110
|
-
dryRun,
|
|
3111
|
-
workflowPath,
|
|
3112
|
-
workflowAction,
|
|
3113
|
-
configAction,
|
|
3114
|
-
requiredSecretName: plan.requiredSecretName,
|
|
3115
|
-
diff,
|
|
3116
|
-
workflowYaml: dryRun ? plan.workflowYaml : null,
|
|
3117
|
-
configSnippetJson: dryRun ? JSON.stringify(plan.configSnippet, null, 2) : null,
|
|
3118
|
-
configError: null,
|
|
3119
|
-
dependabot,
|
|
3120
|
-
dependabotProposal: dependabot === "proposed" ? dependabotFile.content : null,
|
|
3121
|
-
...packageManagerNote !== void 0 ? { packageManagerNote } : {},
|
|
3122
|
-
...BETA_PROVIDERS.includes(provider) ? { betaProvider: provider } : {}
|
|
3123
|
-
};
|
|
3124
|
-
}
|
|
3125
|
-
|
|
3126
|
-
// src/generators/add-backend/generator.ts
|
|
3127
|
-
import { getProjects as getProjects2 } from "@nx/devkit";
|
|
3128
|
-
|
|
3129
|
-
// src/generators/add-backend/render.ts
|
|
3130
|
-
function renderAddBackendBody(report) {
|
|
3131
|
-
const lines = [];
|
|
3132
|
-
if (report.refused === "no-frontend") {
|
|
3133
|
-
lines.push(`Refused: no frontend application and no "${report.apiName}" app found in this workspace.`);
|
|
3134
|
-
lines.push(" add-backend wires a backend app \u2014 beside a frontend, or on its own in a backend-only");
|
|
3135
|
-
lines.push(` workspace (create one with: npx create-nx-workspace --preset=${report.backend} --appName=${report.apiName}).`);
|
|
3136
|
-
lines.push(" If the backend app has another name, pass --apiName=<its directory name>.");
|
|
3137
|
-
return lines.join("\n");
|
|
3138
|
-
}
|
|
3139
|
-
if (report.refused === "frontend-not-found") {
|
|
3140
|
-
lines.push(`Refused: --frontendProject "${report.frontendProject}" does not match any project.`);
|
|
3141
|
-
lines.push(" Nothing was written. Check the name with `nx show projects`.");
|
|
3142
|
-
return lines.join("\n");
|
|
3143
|
-
}
|
|
3144
|
-
if (report.refused === "ambiguous-frontend") {
|
|
3145
|
-
lines.push("Refused: multiple frontend applications found \u2014 add-backend will not guess:");
|
|
3146
|
-
for (const name of report.frontendCandidates ?? []) lines.push(` - ${name}`);
|
|
3147
|
-
lines.push(" Nothing was written. Re-run with --frontendProject=<name> to pick one.");
|
|
3148
|
-
return lines.join("\n");
|
|
3149
|
-
}
|
|
3150
|
-
if (report.refused === "invalid-project-json") {
|
|
3151
|
-
lines.push("Refused: a project.json this run must update could not be parsed:");
|
|
3152
|
-
for (const path of report.invalidProjects ?? []) lines.push(` - ${path}`);
|
|
3153
|
-
lines.push(" Nothing was written \u2014 fix the JSON, then re-run add-backend.");
|
|
3154
|
-
return lines.join("\n");
|
|
3155
|
-
}
|
|
3156
|
-
if (report.refused === "missing-api-project") {
|
|
3157
|
-
lines.push(`Refused: no "${report.apiName}" application project found.`);
|
|
3158
|
-
lines.push(" Create it with the official commands first (copy-paste, names already filled in),");
|
|
3159
|
-
lines.push(" then re-run add-backend to wire it up:");
|
|
3160
|
-
lines.push("");
|
|
3161
|
-
for (const command of report.commands ?? []) lines.push(` ${command}`);
|
|
3162
|
-
if (report.expressRange !== void 0) {
|
|
3163
|
-
lines.push("");
|
|
3164
|
-
lines.push(` The express line declares express@${report.expressRange}, the range this workspace's @nx/react supports \u2014`);
|
|
3165
|
-
lines.push(" the official generator then keeps it instead of adding express 5, which npm rejects as a peer conflict.");
|
|
3166
|
-
}
|
|
3167
|
-
return lines.join("\n");
|
|
3168
|
-
}
|
|
3169
|
-
if (report.packageManagerNote !== void 0) lines.push(` note: ${report.packageManagerNote}`);
|
|
3170
|
-
lines.push(` frontend project: ${report.frontendProject ?? "none (backend-only workspace)"}`);
|
|
3171
|
-
const libPath = report.sharedTypesRoot ?? "libs/shared-types";
|
|
3172
|
-
lines.push(
|
|
3173
|
-
report.sharedTypes === "created" ? ` ${libPath}: created (import it as ${report.sharedTypesPackage})` : report.sharedTypes === "disabled" ? " shared-types lib: none (sharedTypes: false in toolkit.config.json)" : ` ${libPath}: already exists \u2014 skipped`
|
|
3174
|
-
);
|
|
3175
|
-
if (report.config !== void 0 && report.config !== "unchanged") {
|
|
3176
|
-
lines.push(` toolkit.config.json: backend "${report.backend}" recorded (${report.config}) \u2014 doctor's backend checks now run`);
|
|
3177
|
-
}
|
|
3178
|
-
if (report.workspaceGlob !== void 0) {
|
|
3179
|
-
const pmName = report.packageManager ?? "pnpm";
|
|
3180
|
-
const registry = packageManagerCommands(pmName).workspaceRegistration;
|
|
3181
|
-
lines.push(
|
|
3182
|
-
{
|
|
3183
|
-
added: ` ${registry}: '${libPath}' registered \u2014 run \`${pmName} install\` once to link it`,
|
|
3184
|
-
present: ` ${registry}: already covers ${libPath}`,
|
|
3185
|
-
unrecognized: (
|
|
3186
|
-
// an integrated layout lists no workspace packages: the paths alias below links it
|
|
3187
|
-
report.tsconfigPaths === "added" || report.tsconfigPaths === "present" ? ` ${registry}: no workspace package list (integrated layout) \u2014 the tsconfig paths alias links ${libPath}` : ` ${registry}: shape not recognized \u2014 add '${libPath}' to its workspace list yourself`
|
|
3188
|
-
)
|
|
3189
|
-
}[report.workspaceGlob]
|
|
3190
|
-
);
|
|
3191
|
-
}
|
|
3192
|
-
for (const tag of report.tags) {
|
|
3193
|
-
lines.push(` tag ${tag.project}: ${tag.action === "added" ? "added" : "already present"}`);
|
|
3194
|
-
}
|
|
3195
|
-
lines.push(
|
|
3196
|
-
{
|
|
3197
|
-
added: ` tsconfig.base.json paths: ${report.sharedTypesPackage} added (a classic or integrated setup \u2014 libs resolve through paths)`,
|
|
3198
|
-
present: " tsconfig.base.json paths: already wired",
|
|
3199
|
-
"workspace-link": " tsconfig paths: not needed \u2014 the workspace package link is the wiring",
|
|
3200
|
-
unrecognized: " tsconfig.base.json: could not be parsed \u2014 left untouched",
|
|
3201
|
-
none: " tsconfig paths: nothing to wire (no shared-types lib)"
|
|
3202
|
-
}[report.tsconfigPaths]
|
|
3203
|
-
);
|
|
3204
|
-
switch (report.eslint) {
|
|
3205
|
-
case "applied":
|
|
3206
|
-
lines.push(" ESLint boundaries: cross-side deny rules written (existing imports stay allowed)");
|
|
3207
|
-
break;
|
|
3208
|
-
case "already-applied":
|
|
3209
|
-
lines.push(" ESLint boundaries: already enforced \u2014 no change");
|
|
3210
|
-
break;
|
|
3211
|
-
case "degraded-unrecognized":
|
|
3212
|
-
lines.push(" ESLint boundaries: your config was NOT recognized \u2014 left untouched.");
|
|
3213
|
-
lines.push(" Merge this yourself (then boundaries are actually enforced):");
|
|
3214
|
-
break;
|
|
3215
|
-
case "snippet-only":
|
|
3216
|
-
lines.push(" ESLint boundaries: report-only (pass --applyEslint to write them).");
|
|
3217
|
-
lines.push(" Paste-ready snippet:");
|
|
3218
|
-
break;
|
|
3219
|
-
}
|
|
3220
|
-
if (report.snippet) {
|
|
3221
|
-
lines.push("");
|
|
3222
|
-
lines.push(report.snippet);
|
|
3223
|
-
}
|
|
3224
|
-
return lines.join("\n");
|
|
3225
|
-
}
|
|
3226
|
-
|
|
3227
|
-
// src/generators/add-backend/generator.ts
|
|
3228
|
-
function runAddBackend(tree, options) {
|
|
3229
|
-
const backend = options.backend;
|
|
3230
|
-
const apiName = options.apiName ?? "api";
|
|
3231
|
-
const applyEslint = options.applyEslint ?? false;
|
|
3232
|
-
const dryRun = options.dryRun ?? false;
|
|
3233
|
-
const write = !dryRun;
|
|
3234
|
-
const base = { backend, apiName };
|
|
3235
|
-
const refusalShape = {
|
|
3236
|
-
sharedTypes: "skipped",
|
|
3237
|
-
tags: [],
|
|
3238
|
-
tsconfigPaths: "unrecognized",
|
|
3239
|
-
eslint: "snippet-only"
|
|
3240
|
-
};
|
|
3241
|
-
let projects;
|
|
3242
|
-
try {
|
|
3243
|
-
projects = getProjects2(tree);
|
|
3244
|
-
} catch (error) {
|
|
3245
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
3246
|
-
const pathMatch = /Cannot parse ([^:]+):/.exec(message);
|
|
3247
|
-
return {
|
|
3248
|
-
...base,
|
|
3249
|
-
refused: "invalid-project-json",
|
|
3250
|
-
invalidProjects: [pathMatch ? pathMatch[1] : message],
|
|
3251
|
-
...refusalShape
|
|
3252
|
-
};
|
|
3253
|
-
}
|
|
3254
|
-
const detectedPm = treePackageManager(tree, readToolkitConfig(tree));
|
|
3255
|
-
const packageManager = detectedPm ?? "pnpm";
|
|
3256
|
-
const pmCommands = packageManagerCommands(
|
|
3257
|
-
packageManager,
|
|
3258
|
-
packageManager === "yarn" ? detectYarnFlavor((p) => tree.exists(p), (p) => tree.read(p, "utf-8")) : void 0
|
|
3259
|
-
);
|
|
3260
|
-
const packageManagerNote = detectedPm === null ? "the package manager could not be detected (no pnpm, yarn, bun or npm markers) \u2014 commands below assume pnpm." : void 0;
|
|
3261
|
-
const baseNameOf2 = (root) => root.split("/").pop() ?? root;
|
|
3262
|
-
const entries = [...projects.entries()];
|
|
3263
|
-
const declaredApp = ([, config]) => config.projectType === "application";
|
|
3264
|
-
const inferredApp = ([, config]) => config.projectType === void 0 && config.root !== "" && config.root !== "." && resolveProjectType(tree, config.root, void 0) === "application";
|
|
3265
|
-
const apiNamed = entries.filter(([, config]) => baseNameOf2(config.root) === apiName);
|
|
3266
|
-
const apiEntry = apiNamed.find(declaredApp) ?? apiNamed.find((entry) => inferredApp(entry) && tree.exists(`${entry[1].root}/tsconfig.app.json`));
|
|
3267
|
-
let frontendProject;
|
|
3268
|
-
if (options.frontendProject !== void 0) {
|
|
3269
|
-
if (!projects.has(options.frontendProject)) {
|
|
3270
|
-
return { ...base, refused: "frontend-not-found", frontendProject: options.frontendProject, ...refusalShape };
|
|
3271
|
-
}
|
|
3272
|
-
frontendProject = options.frontendProject;
|
|
3273
|
-
} else {
|
|
3274
|
-
const eligible = entries.filter(([, config]) => {
|
|
3275
|
-
const dir = baseNameOf2(config.root);
|
|
3276
|
-
return dir !== apiName && dir !== "e2e" && !dir.endsWith("-e2e");
|
|
3277
|
-
});
|
|
3278
|
-
const declared = eligible.filter(declaredApp);
|
|
3279
|
-
const candidates = (declared.length > 0 ? declared : eligible.filter(inferredApp)).map(([name]) => name).sort();
|
|
3280
|
-
if (candidates.length === 0 && !apiEntry) {
|
|
3281
|
-
return { ...base, refused: "no-frontend", ...refusalShape };
|
|
3282
|
-
}
|
|
3283
|
-
if (candidates.length > 1) {
|
|
3284
|
-
return { ...base, refused: "ambiguous-frontend", frontendCandidates: candidates, ...refusalShape };
|
|
3285
|
-
}
|
|
3286
|
-
frontendProject = candidates[0];
|
|
3287
|
-
}
|
|
3288
|
-
if (!apiEntry) {
|
|
3289
|
-
if (frontendProject === void 0) return { ...base, refused: "no-frontend", ...refusalShape };
|
|
3290
|
-
let nxPin = "";
|
|
3291
|
-
let expressDeclared = false;
|
|
3292
|
-
const rootPkgRaw = tree.read("package.json", "utf-8");
|
|
3293
|
-
if (rootPkgRaw !== null) {
|
|
3294
|
-
try {
|
|
3295
|
-
const pkg = JSON.parse(rootPkgRaw);
|
|
3296
|
-
const nxVersion = pkg.devDependencies?.["nx"] ?? pkg.dependencies?.["nx"];
|
|
3297
|
-
if (nxVersion && /^[~^]?\d/.test(nxVersion)) nxPin = `@${nxVersion}`;
|
|
3298
|
-
expressDeclared = pkg.dependencies?.["express"] !== void 0 || pkg.devDependencies?.["express"] !== void 0;
|
|
3299
|
-
} catch {
|
|
3300
|
-
}
|
|
3301
|
-
}
|
|
3302
|
-
const frontendRoot = projects.get(frontendProject)?.root ?? "";
|
|
3303
|
-
const frontendParent = frontendRoot.split("/").slice(0, -1).join("/");
|
|
3304
|
-
const apiDirectory = frontendRoot === "" || frontendRoot === "." ? `apps/${apiName}` : `${frontendParent === "" ? "" : `${frontendParent}/`}${apiName}`;
|
|
3305
|
-
const expressAlignment = backend === "express" && !expressDeclared ? readExpressPeerRange(tree) : void 0;
|
|
3306
|
-
const quotedExpress = expressAlignment ? `"express@${expressAlignment}"` : void 0;
|
|
3307
|
-
return {
|
|
3308
|
-
...base,
|
|
3309
|
-
refused: "missing-api-project",
|
|
3310
|
-
frontendProject,
|
|
3311
|
-
...expressAlignment !== void 0 ? { expressRange: expressAlignment } : {},
|
|
3312
|
-
commands: [
|
|
3313
|
-
pmCommands.addDevAtRoot(`@nx/${backend}${nxPin}`),
|
|
3314
|
-
...quotedExpress ? [pmCommands.addAtRoot(quotedExpress)] : [],
|
|
3315
|
-
`${pmCommands.runNx} g @nx/${backend}:application ${apiDirectory} --frontendProject=${frontendProject}`
|
|
3316
|
-
],
|
|
3317
|
-
...refusalShape
|
|
3318
|
-
};
|
|
3319
|
-
}
|
|
3320
|
-
const recordedConfig = readToolkitConfigFile(tree).parsed;
|
|
3321
|
-
const boundaryTags = readBoundaryTags(recordedConfig);
|
|
3322
|
-
const sharedTypesConfig = readSharedTypesConfig(recordedConfig);
|
|
3323
|
-
const withSharedTypes = sharedTypesConfig !== false;
|
|
3324
|
-
const sharedTypesRoot = sharedTypesConfig === false ? "" : sharedTypesConfig.path;
|
|
3325
|
-
const sharedTypesExists = withSharedTypes && (tree.exists(`${sharedTypesRoot}/package.json`) || tree.exists(`${sharedTypesRoot}/project.json`));
|
|
3326
|
-
const rootPackageJson = tree.read("package.json", "utf-8");
|
|
3327
|
-
const workspaceName = rootPackageJson ? JSON.parse(rootPackageJson).name ?? "workspace" : "workspace";
|
|
3328
|
-
const wiring = composeBackendWiring({ workspaceName, tags: boundaryTags, ...sharedTypesConfig === false ? {} : { sharedTypes: sharedTypesConfig } });
|
|
3329
|
-
const [apiProjectName, apiConfig] = apiEntry;
|
|
3330
|
-
const tagTargets = [
|
|
3331
|
-
...frontendProject === void 0 ? [] : [{ project: frontendProject, root: projects.get(frontendProject)?.root ?? `apps/${frontendProject}` }],
|
|
3332
|
-
{ project: apiProjectName, root: apiConfig.root }
|
|
3333
|
-
];
|
|
3334
|
-
if (sharedTypesExists) tagTargets.push({ project: wiring.sharedTypesPackageName, root: sharedTypesRoot });
|
|
3335
|
-
const parsedTargets = /* @__PURE__ */ new Map();
|
|
3336
|
-
const invalidProjects = [];
|
|
3337
|
-
for (const target of tagTargets) {
|
|
3338
|
-
const projectJsonPath = `${target.root}/project.json`;
|
|
3339
|
-
const packageJsonPath = `${target.root}/package.json`;
|
|
3340
|
-
const kind = tree.exists(projectJsonPath) ? "project" : "package";
|
|
3341
|
-
const path = kind === "project" ? projectJsonPath : packageJsonPath;
|
|
3342
|
-
const raw = tree.read(path, "utf-8");
|
|
3343
|
-
if (raw === null) {
|
|
3344
|
-
invalidProjects.push(path);
|
|
3345
|
-
continue;
|
|
3346
|
-
}
|
|
3347
|
-
try {
|
|
3348
|
-
parsedTargets.set(target.project, { path, raw, kind, parsed: JSON.parse(raw) });
|
|
3349
|
-
} catch {
|
|
3350
|
-
invalidProjects.push(path);
|
|
3351
|
-
}
|
|
3352
|
-
}
|
|
3353
|
-
if (invalidProjects.length > 0) {
|
|
3354
|
-
return { ...base, refused: "invalid-project-json", frontendProject, invalidProjects, ...refusalShape };
|
|
3355
|
-
}
|
|
3356
|
-
const sharedTypes = !withSharedTypes ? "disabled" : sharedTypesExists ? "skipped" : "created";
|
|
3357
|
-
if (sharedTypes === "created" && write) {
|
|
3358
|
-
for (const file of wiring.sharedTypesFiles) tree.write(file.filePath, file.content);
|
|
3359
|
-
}
|
|
3360
|
-
let configOutcome = "unchanged";
|
|
3361
|
-
{
|
|
3362
|
-
const { raw: configRaw, parsed: configParsed } = readToolkitConfigFile(tree);
|
|
3363
|
-
const parseOk = configRaw === null || configParsed !== null;
|
|
3364
|
-
const existing = configParsed ?? {};
|
|
3365
|
-
if (parseOk && existing["backend"] !== backend) {
|
|
3366
|
-
configOutcome = configRaw === null ? "created" : "updated";
|
|
3367
|
-
if (write) tree.write(TOOLKIT_CONFIG_PATH, applyJsonEdits(configRaw, [[["backend"], backend]]));
|
|
3368
|
-
}
|
|
3369
|
-
}
|
|
3370
|
-
let workspaceGlob = withSharedTypes ? "unrecognized" : void 0;
|
|
3371
|
-
const workspaceYaml = withSharedTypes ? tree.read("pnpm-workspace.yaml", "utf-8") : null;
|
|
3372
|
-
if (!withSharedTypes) {
|
|
3373
|
-
} else if (workspaceYaml !== null) {
|
|
3374
|
-
const globResult = addSharedTypesWorkspaceGlob(workspaceYaml, sharedTypesRoot);
|
|
3375
|
-
workspaceGlob = globResult.status;
|
|
3376
|
-
if (globResult.status === "added" && write) tree.write("pnpm-workspace.yaml", globResult.text);
|
|
3377
|
-
} else if ((detectedPm === "npm" || detectedPm === "yarn" || detectedPm === "bun") && rootPackageJson !== null) {
|
|
3378
|
-
const yarnNeedsPrivate = detectedPm === "yarn" && JSON.parse(rootPackageJson).private !== true;
|
|
3379
|
-
const npmResult = yarnNeedsPrivate ? { status: "unrecognized" } : addSharedTypesNpmWorkspace(rootPackageJson, sharedTypesRoot);
|
|
3380
|
-
workspaceGlob = npmResult.status;
|
|
3381
|
-
if (npmResult.status === "added" && write) tree.write("package.json", npmResult.text);
|
|
3382
|
-
}
|
|
3383
|
-
const mergeTag = (project, tag) => {
|
|
3384
|
-
const target = parsedTargets.get(project);
|
|
3385
|
-
if (!target) return { project, action: "added" };
|
|
3386
|
-
const holder = target.kind === "project" ? target.parsed : target.parsed.nx ??= {};
|
|
3387
|
-
const tags2 = holder.tags ?? [];
|
|
3388
|
-
if (tags2.includes(tag)) return { project, action: "present" };
|
|
3389
|
-
holder.tags = [...tags2, tag];
|
|
3390
|
-
target.raw = appendJsonArrayItem(target.raw, target.kind === "project" ? ["tags"] : ["nx", "tags"], tag);
|
|
3391
|
-
if (write) tree.write(target.path, target.raw);
|
|
3392
|
-
return { project, action: "added" };
|
|
3393
|
-
};
|
|
3394
|
-
const tags = [
|
|
3395
|
-
...frontendProject === void 0 ? [] : [mergeTag(frontendProject, wiring.tagByRole.frontend)],
|
|
3396
|
-
mergeTag(apiProjectName, wiring.tagByRole.backend),
|
|
3397
|
-
...withSharedTypes ? [mergeTag(wiring.sharedTypesPackageName, wiring.tagByRole.shared)] : []
|
|
3398
|
-
];
|
|
3399
|
-
let tsconfigPaths = withSharedTypes ? "workspace-link" : "none";
|
|
3400
|
-
const tsconfigRaw = withSharedTypes ? tree.read("tsconfig.base.json", "utf-8") : null;
|
|
3401
|
-
if (tsconfigRaw !== null) {
|
|
3402
|
-
try {
|
|
3403
|
-
const tsconfig = JSON.parse(tsconfigRaw);
|
|
3404
|
-
const baseUrl = tsconfig.compilerOptions?.baseUrl;
|
|
3405
|
-
if (tsconfig.compilerOptions && (baseUrl || tsconfig.compilerOptions.paths !== void 0)) {
|
|
3406
|
-
const paths = tsconfig.compilerOptions.paths ?? {};
|
|
3407
|
-
if (paths[wiring.sharedTypesPackageName]) {
|
|
3408
|
-
tsconfigPaths = "present";
|
|
3409
|
-
} else {
|
|
3410
|
-
tsconfigPaths = "added";
|
|
3411
|
-
const entry = [`${baseUrl ? "" : "./"}${sharedTypesRoot}/src/index.ts`];
|
|
3412
|
-
if (write) tree.write("tsconfig.base.json", setJsonValue(tsconfigRaw, ["compilerOptions", "paths", wiring.sharedTypesPackageName], entry));
|
|
3413
|
-
}
|
|
3414
|
-
}
|
|
3415
|
-
} catch {
|
|
3416
|
-
tsconfigPaths = "unrecognized";
|
|
3417
|
-
}
|
|
3418
|
-
}
|
|
3419
|
-
let eslint;
|
|
3420
|
-
let snippet;
|
|
3421
|
-
if (!applyEslint) {
|
|
3422
|
-
eslint = "snippet-only";
|
|
3423
|
-
snippet = buildBoundarySnippet(boundaryTags);
|
|
3424
|
-
} else {
|
|
3425
|
-
const configPath = ESLINT_CONFIG_CANDIDATES.find((candidate) => tree.exists(candidate));
|
|
3426
|
-
const configText = configPath ? tree.read(configPath, "utf-8") : null;
|
|
3427
|
-
const result = configText !== null ? applyBoundaryConstraints(configText, boundaryTags) : { status: "unrecognized" };
|
|
3428
|
-
if (result.status === "applied") {
|
|
3429
|
-
eslint = "applied";
|
|
3430
|
-
if (write && configPath) tree.write(configPath, result.text);
|
|
3431
|
-
} else if (result.status === "already-applied") {
|
|
3432
|
-
eslint = "already-applied";
|
|
3433
|
-
} else {
|
|
3434
|
-
eslint = "degraded-unrecognized";
|
|
3435
|
-
snippet = buildBoundarySnippet(boundaryTags);
|
|
3436
|
-
}
|
|
3437
|
-
}
|
|
3438
|
-
return {
|
|
3439
|
-
...base,
|
|
3440
|
-
frontendProject,
|
|
3441
|
-
sharedTypes,
|
|
3442
|
-
tags,
|
|
3443
|
-
workspaceGlob,
|
|
3444
|
-
tsconfigPaths,
|
|
3445
|
-
eslint,
|
|
3446
|
-
snippet,
|
|
3447
|
-
sharedTypesPackage: withSharedTypes ? wiring.sharedTypesPackageName : void 0,
|
|
3448
|
-
sharedTypesRoot: withSharedTypes ? sharedTypesRoot : void 0,
|
|
3449
|
-
config: configOutcome,
|
|
3450
|
-
packageManager,
|
|
3451
|
-
...packageManagerNote !== void 0 ? { packageManagerNote } : {}
|
|
3452
|
-
};
|
|
3453
|
-
}
|
|
3454
|
-
function readExpressPeerRange(tree) {
|
|
3455
|
-
const raw = tree.read("node_modules/@nx/react/package.json", "utf-8");
|
|
3456
|
-
if (raw === null) return void 0;
|
|
3457
|
-
try {
|
|
3458
|
-
const range = JSON.parse(raw).peerDependencies?.["express"];
|
|
3459
|
-
return typeof range === "string" && range.trim().length > 0 ? range.trim() : void 0;
|
|
3460
|
-
} catch {
|
|
3461
|
-
return void 0;
|
|
3462
|
-
}
|
|
3463
|
-
}
|
|
3464
|
-
|
|
3465
157
|
// src/generators/init/render.ts
|
|
3466
158
|
var SECTION_TITLES = {
|
|
3467
159
|
fsd: "add-fsd-lite",
|
|
@@ -3916,7 +608,7 @@ async function initGenerator(tree, options, deps = { isWorkingTreeClean: gitIsWo
|
|
|
3916
608
|
}
|
|
3917
609
|
const workflowPaths = collectWrittenWorkflowPaths(report);
|
|
3918
610
|
const root = tree.root;
|
|
3919
|
-
const graphOutputFile =
|
|
611
|
+
const graphOutputFile = join(tmpdir(), `nx-archweave-init-graph-${Date.now()}.json`);
|
|
3920
612
|
return () => {
|
|
3921
613
|
runPostInitValidation({ root, workflowPaths, graphOutputFile }, createProcessExec());
|
|
3922
614
|
if (strictFailure !== null) throw strictFailure;
|