@clearlydev/nx-archweave 0.1.1 → 0.2.1

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.
Files changed (54) hide show
  1. package/README.md +1 -1
  2. package/dist/eslint/fsd-boundaries.d.ts +25 -8
  3. package/dist/eslint/fsd-policy.d.ts +38 -0
  4. package/dist/eslint/index.d.ts +10 -9
  5. package/dist/eslint/index.js +227 -34
  6. package/dist/eslint/slice-groups.d.ts +12 -0
  7. package/dist/generators/add-agent-rules/generator.js +1175 -23
  8. package/dist/generators/add-backend/generator.js +1127 -75
  9. package/dist/generators/add-fsd-lite/generator.js +1274 -96
  10. package/dist/generators/add-review/files/dependabot.yml +7 -0
  11. package/dist/generators/add-review/generator.js +1100 -46
  12. package/dist/generators/doctor/generator.js +780 -96
  13. package/dist/generators/entity/generator.js +251 -32
  14. package/dist/generators/feature/generator.js +251 -32
  15. package/dist/generators/init/generator.js +1955 -549
  16. package/dist/generators/page/generator.js +251 -32
  17. package/dist/generators/shared/generator.js +251 -32
  18. package/dist/generators/widget/generator.js +251 -32
  19. package/package.json +4 -1
  20. package/src/generators/add-agent-rules/schema.json +5 -0
  21. package/src/generators/add-fsd-lite/schema.json +2 -2
  22. package/src/generators/entity/schema.json +11 -1
  23. package/src/generators/feature/schema.json +11 -1
  24. package/src/generators/init/schema.json +13 -2
  25. package/src/generators/page/schema.json +15 -3
  26. package/src/generators/shared/schema.json +7 -1
  27. package/src/generators/widget/schema.json +11 -1
  28. package/dist/eslint/fsd-boundaries.js +0 -144
  29. package/dist/eslint/tsconfig-aliases.js +0 -114
  30. package/dist/generators/add-agent-rules/render.js +0 -46
  31. package/dist/generators/add-backend/render.js +0 -102
  32. package/dist/generators/add-fsd-lite/render.js +0 -72
  33. package/dist/generators/add-github-templates/render.js +0 -26
  34. package/dist/generators/add-integration/render.js +0 -34
  35. package/dist/generators/add-review/render.js +0 -96
  36. package/dist/generators/allow-builds/render.js +0 -82
  37. package/dist/generators/doctor/integration-report.js +0 -70
  38. package/dist/generators/doctor/render.js +0 -10
  39. package/dist/generators/init/render.js +0 -125
  40. package/dist/generators/init/validation.js +0 -74
  41. package/dist/generators/install-integration/render.js +0 -128
  42. package/dist/generators/record-integration/render.js +0 -31
  43. package/dist/generators/shared/dry-run.js +0 -12
  44. package/dist/generators/shared/frontend-app.js +0 -57
  45. package/dist/generators/shared/project-type.js +0 -39
  46. package/dist/generators/shared/record-pipeline.js +0 -34
  47. package/dist/generators/shared/run-nx.js +0 -24
  48. package/dist/generators/shared/template-files.js +0 -38
  49. package/dist/generators/shared/toolkit-config-file.js +0 -110
  50. package/dist/generators/slice/core.js +0 -185
  51. package/dist/generators/slice/render.js +0 -70
  52. package/dist/lib/nx-plugin.js +0 -7
  53. package/dist/runtime/integrations-composition-root.js +0 -20
  54. package/dist/runtime/node-command-runner.js +0 -81
@@ -169,7 +169,7 @@ import { existsSync as existsSync6 } from "node:fs";
169
169
  import { join as join6 } from "node:path";
170
170
 
171
171
  // ../template-engine/dist/doctor/review-checks.js
172
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
172
+ import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2 } from "node:fs";
173
173
  import { join as join2 } from "node:path";
174
174
 
175
175
  // ../review-policy/dist/modes/review-modes.js
@@ -291,6 +291,20 @@ function buildOverEngineeringPrompt(locale) {
291
291
  // ../review-policy/dist/verdict-gate/verdict-gate.js
292
292
  var ROUND_MARKER = "<!-- archweave-review-round -->";
293
293
 
294
+ // ../template-engine/dist/review/action-pins.js
295
+ var ACTION_PINS = {
296
+ "actions/checkout": "actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1",
297
+ "actions/setup-node": "actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0",
298
+ "pnpm/action-setup": "pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0",
299
+ "oven-sh/setup-bun": "oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0",
300
+ "anthropics/claude-code-action": "anthropics/claude-code-action@6b6870e649b5ff408753befeaee5fc7989d4e6a9 # v1.0.258",
301
+ "google-github-actions/run-gemini-cli": "google-github-actions/run-gemini-cli@f77273f4c914e4bf38440cf36a0369cb64a37489 # v0.1.22",
302
+ "openai/codex-action": "openai/codex-action@bdf19a4a223ec2549a3e2274a0cf61556bc07675 # v1.13"
303
+ };
304
+ function pinnedUses(action) {
305
+ return ACTION_PINS[action];
306
+ }
307
+
294
308
  // ../template-engine/dist/review/workflow-skeleton.js
295
309
  function indentBlock(text, spaces) {
296
310
  const pad = " ".repeat(spaces);
@@ -309,6 +323,17 @@ function renderConcurrencyBlock() {
309
323
  function renderPermissionsBlock() {
310
324
  return ["permissions:", " contents: read", " pull-requests: write", " id-token: write"].join("\n");
311
325
  }
326
+ function renderCheckoutStep(gateCondition, extraWith = []) {
327
+ return [
328
+ "- name: Checkout repository",
329
+ ` if: ${gateCondition}`,
330
+ ` uses: ${pinnedUses("actions/checkout")}`,
331
+ " with:",
332
+ ...extraWith.map((line) => ` ${line}`),
333
+ " fetch-depth: 1",
334
+ " persist-credentials: false"
335
+ ].join("\n");
336
+ }
312
337
  function renderSecretGateStep(secretName) {
313
338
  return [
314
339
  "- name: Check review secret",
@@ -332,7 +357,7 @@ function renderRoundGateStep(maxRounds) {
332
357
  " env:",
333
358
  " GH_TOKEN: ${{ github.token }}",
334
359
  " run: |",
335
- ` counts=$(gh api "repos/\${{ github.repository }}/issues/\${{ github.event.pull_request.number }}/comments" --paginate --jq '[([.[] | select(.body | contains("${ROUND_MARKER}"))] | length), ([.[].id] | max // 0)]' | jq -s -r '[(map(.[0]) | add // 0), (map(.[1]) | max // 0)] | @tsv')`,
360
+ ` 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')`,
336
361
  ` rounds=$(printf '%s' "$counts" | cut -f1)`,
337
362
  ` echo "last-comment-id=$(printf '%s' "$counts" | cut -f2)" >> "$GITHUB_OUTPUT"`,
338
363
  ` if [ "$rounds" -ge ${maxRounds} ]; then`,
@@ -364,8 +389,7 @@ function renderVerdictGateStep(maxRounds, locale) {
364
389
  ].join("\n");
365
390
  }
366
391
  var SETUP_NODE_VERSION = "22";
367
- var SETUP_PNPM_VERSION = "10";
368
- var SETUP_BUN_ACTION = "oven-sh/setup-bun@v2";
392
+ var FALLBACK_PNPM_VERSION = "10";
369
393
  var INSTALL_LINES = {
370
394
  pnpm: [" pnpm install --frozen-lockfile"],
371
395
  npm: [" npm ci"],
@@ -375,19 +399,39 @@ var INSTALL_LINES = {
375
399
  ],
376
400
  bun: [" bun install --frozen-lockfile"]
377
401
  };
402
+ function renderPnpmVersionStep(gate) {
403
+ return [
404
+ "- name: Detect pnpm version",
405
+ " id: pnpm-version",
406
+ gate,
407
+ " run: |",
408
+ ` if [ -n "$(jq -r '.packageManager // empty' package.json)" ]; then`,
409
+ ' echo "version=" >> "$GITHUB_OUTPUT"',
410
+ " else",
411
+ ` echo "version=${FALLBACK_PNPM_VERSION}" >> "$GITHUB_OUTPUT"`,
412
+ " fi"
413
+ ].join("\n");
414
+ }
378
415
  function renderWorkspaceSetupSteps(packageManager) {
379
416
  const gate = " if: steps.secret-gate.outputs.enabled == 'true' && steps.round-gate.outputs.allowed == 'true'";
380
417
  const steps = [];
381
418
  if (packageManager === "pnpm") {
382
- steps.push(["- name: Set up pnpm", gate, " uses: pnpm/action-setup@v4", " with:", ` version: ${SETUP_PNPM_VERSION}`].join("\n"));
419
+ steps.push(renderPnpmVersionStep(gate));
420
+ steps.push([
421
+ "- name: Set up pnpm",
422
+ gate,
423
+ ` uses: ${pinnedUses("pnpm/action-setup")}`,
424
+ " with:",
425
+ " version: ${{ steps.pnpm-version.outputs.version }}"
426
+ ].join("\n"));
383
427
  }
384
428
  if (packageManager === "bun") {
385
- steps.push(["- name: Set up bun", gate, ` uses: ${SETUP_BUN_ACTION}`].join("\n"));
429
+ steps.push(["- name: Set up bun", gate, ` uses: ${pinnedUses("oven-sh/setup-bun")}`].join("\n"));
386
430
  }
387
431
  steps.push([
388
432
  "- name: Set up Node",
389
433
  gate,
390
- " uses: actions/setup-node@v4",
434
+ ` uses: ${pinnedUses("actions/setup-node")}`,
391
435
  " with:",
392
436
  ` node-version: ${SETUP_NODE_VERSION}`,
393
437
  // yarn: no setup-node cache — it would query the runner's global yarn 1 even in a
@@ -439,17 +483,11 @@ function buildWorkflowYaml(input) {
439
483
  const steps = [
440
484
  renderSecretGateStep(SECRET_NAME),
441
485
  renderRoundGateStep(input.maxRounds),
442
- [
443
- "- name: Checkout repository",
444
- " if: steps.secret-gate.outputs.enabled == 'true' && steps.round-gate.outputs.allowed == 'true'",
445
- " uses: actions/checkout@v4",
446
- " with:",
447
- " fetch-depth: 1"
448
- ].join("\n"),
486
+ renderCheckoutStep("steps.secret-gate.outputs.enabled == 'true' && steps.round-gate.outputs.allowed == 'true'"),
449
487
  [
450
488
  "- name: Run Claude review",
451
489
  " if: steps.secret-gate.outputs.enabled == 'true' && steps.round-gate.outputs.allowed == 'true'",
452
- " uses: anthropics/claude-code-action@v1",
490
+ ` uses: ${pinnedUses("anthropics/claude-code-action")}`,
453
491
  " with:",
454
492
  ` claude_code_oauth_token: \${{ secrets.${SECRET_NAME} }}`,
455
493
  " prompt: |",
@@ -519,13 +557,7 @@ function buildWorkflowYaml2(input) {
519
557
  const steps = [
520
558
  renderSecretGateStep(SECRET_NAME2),
521
559
  renderRoundGateStep(input.maxRounds),
522
- [
523
- "- name: Checkout repository",
524
- ` if: ${gateCondition}`,
525
- " uses: actions/checkout@v4",
526
- " with:",
527
- " fetch-depth: 1"
528
- ].join("\n"),
560
+ renderCheckoutStep(gateCondition),
529
561
  [
530
562
  "- name: Install cursor-agent CLI",
531
563
  ` if: ${gateCondition}`,
@@ -605,17 +637,11 @@ function buildWorkflowYaml3(input) {
605
637
  const steps = [
606
638
  renderSecretGateStep(SECRET_NAME3),
607
639
  renderRoundGateStep(input.maxRounds),
608
- [
609
- "- name: Checkout repository",
610
- " if: steps.secret-gate.outputs.enabled == 'true' && steps.round-gate.outputs.allowed == 'true'",
611
- " uses: actions/checkout@v4",
612
- " with:",
613
- " fetch-depth: 1"
614
- ].join("\n"),
640
+ renderCheckoutStep("steps.secret-gate.outputs.enabled == 'true' && steps.round-gate.outputs.allowed == 'true'"),
615
641
  [
616
642
  "- name: Run Gemini review",
617
643
  " if: steps.secret-gate.outputs.enabled == 'true' && steps.round-gate.outputs.allowed == 'true'",
618
- " uses: google-github-actions/run-gemini-cli@v0",
644
+ ` uses: ${pinnedUses("google-github-actions/run-gemini-cli")}`,
619
645
  " env:",
620
646
  " GITHUB_TOKEN: ${{ github.token }}",
621
647
  " with:",
@@ -684,15 +710,7 @@ function buildWorkflowYaml4(input) {
684
710
  const steps = [
685
711
  renderSecretGateStep(SECRET_NAME4),
686
712
  renderRoundGateStep(input.maxRounds),
687
- [
688
- "- name: Checkout repository",
689
- ` if: ${gateCondition}`,
690
- " uses: actions/checkout@v4",
691
- " with:",
692
- " ref: refs/pull/${{ github.event.pull_request.number }}/merge",
693
- " fetch-depth: 1",
694
- " persist-credentials: false"
695
- ].join("\n"),
713
+ renderCheckoutStep(gateCondition, ["ref: refs/pull/${{ github.event.pull_request.number }}/merge"]),
696
714
  [
697
715
  "- name: Fetch base and head refs for the PR",
698
716
  ` if: ${gateCondition}`,
@@ -710,7 +728,7 @@ function buildWorkflowYaml4(input) {
710
728
  "- name: Run Codex review",
711
729
  " id: codex-review",
712
730
  ` if: ${gateCondition}`,
713
- " uses: openai/codex-action@v1",
731
+ ` uses: ${pinnedUses("openai/codex-action")}`,
714
732
  " with:",
715
733
  ` openai-api-key: \${{ secrets.${SECRET_NAME4} }}`,
716
734
  ' permission-profile: ":read-only"',
@@ -830,27 +848,88 @@ function validateReviewConfig(review) {
830
848
  var REVIEW_CHECK_IDS = [
831
849
  "review-workflow-present",
832
850
  "review-alt-file-coexistence",
833
- "review-config-consistency"
851
+ "review-config-consistency",
852
+ "review-workflow-providers",
853
+ "review-workflow-pnpm"
834
854
  ];
855
+ var WORKFLOWS_DIR = ".github/workflows";
856
+ function archweaveWorkflows(root) {
857
+ let names;
858
+ try {
859
+ names = readdirSync(join2(root, WORKFLOWS_DIR));
860
+ } catch {
861
+ return [];
862
+ }
863
+ return names.filter((name) => /\.ya?ml$/.test(name)).map((name) => `${WORKFLOWS_DIR}/${name}`).filter((path) => readFileSync2(join2(root, path), "utf-8").includes(ROUND_MARKER)).sort();
864
+ }
865
+ function providerOfWorkflow(path) {
866
+ const main = path.replace(/\.archweave\.yml$/, ".yml");
867
+ return Object.values(REVIEW_PROVIDERS).find((provider) => provider.workflowFileName === main)?.id ?? null;
868
+ }
869
+ function buildReviewProvidersCheck(workflows) {
870
+ const id = "review-workflow-providers";
871
+ const label = "One review provider";
872
+ const providers = new Set(workflows.map(providerOfWorkflow).filter((provider) => provider !== null));
873
+ if (workflows.length === 0)
874
+ return createDoctorCheck({ id, label, status: "not-applicable", message: "no nx-archweave review workflow", evidence: [] });
875
+ if (providers.size <= 1)
876
+ return createDoctorCheck({ id, label, status: "pass", message: "one provider's review workflow", evidence: workflows });
877
+ return createDoctorCheck({
878
+ id,
879
+ label,
880
+ status: "warning",
881
+ message: `review workflows of ${providers.size} providers (${[...providers].join(", ")}) \u2014 each runs on every pull request`,
882
+ evidence: workflows,
883
+ suggestedAction: "Delete the workflow of the provider you no longer use."
884
+ });
885
+ }
886
+ function buildReviewPnpmCheck(root, workflows) {
887
+ const id = "review-workflow-pnpm";
888
+ const label = "Review workflow pnpm setup";
889
+ const fixed = workflows.filter((path) => /pnpm\/action-setup@[^\n]*\n\s*with:\s*\n\s*version:\s*['"]?\d/.test(readFileSync2(join2(root, path), "utf-8")));
890
+ if (fixed.length === 0)
891
+ return createDoctorCheck({ id, label, status: "not-applicable", message: "no review workflow pins a pnpm version", evidence: [] });
892
+ let packageManager;
893
+ try {
894
+ packageManager = JSON.parse(readFileSync2(join2(root, "package.json"), "utf-8"))["packageManager"];
895
+ } catch {
896
+ packageManager = void 0;
897
+ }
898
+ if (typeof packageManager !== "string") {
899
+ return createDoctorCheck({ id, label, status: "pass", message: "the workflow pins pnpm and package.json has no packageManager field", evidence: fixed });
900
+ }
901
+ return createDoctorCheck({
902
+ id,
903
+ label,
904
+ status: "warning",
905
+ message: `the review workflow pins a pnpm version while package.json says "${packageManager}" \u2014 pnpm/action-setup fails on the mismatch`,
906
+ evidence: fixed,
907
+ suggestedAction: "Regenerate the workflow: nx g @clearlydev/nx-archweave:add-review --onConflict=overwrite."
908
+ });
909
+ }
835
910
  function buildReviewChecks(root) {
836
911
  const config = readToolkitConfigState(root);
837
912
  const mainWorkflowPath = REVIEW_PROVIDERS[resolveProviderId(config)].workflowFileName;
838
913
  const altWorkflowPath = deriveAltWorkflowPath(mainWorkflowPath);
839
914
  const mainExists = existsSync2(join2(root, mainWorkflowPath));
840
915
  const altExists = existsSync2(join2(root, altWorkflowPath));
916
+ const workflows = archweaveWorkflows(root);
917
+ const mainIsOurs = workflows.includes(mainWorkflowPath);
841
918
  return [
842
- buildReviewWorkflowPresentCheck(mainWorkflowPath, mainExists, config),
919
+ buildReviewWorkflowPresentCheck(mainWorkflowPath, mainExists, mainIsOurs, config),
843
920
  buildReviewAltFileCoexistenceCheck(mainWorkflowPath, altWorkflowPath, mainExists, altExists),
844
- buildReviewConfigConsistencyCheck(root, mainWorkflowPath, mainExists, config)
921
+ buildReviewConfigConsistencyCheck(root, mainWorkflowPath, mainExists, config),
922
+ buildReviewProvidersCheck(workflows),
923
+ buildReviewPnpmCheck(root, workflows)
845
924
  ];
846
925
  }
847
- function buildReviewWorkflowPresentCheck(mainWorkflowPath, mainExists, config) {
926
+ function buildReviewWorkflowPresentCheck(mainWorkflowPath, mainExists, mainIsOurs, config) {
848
927
  if (mainExists) {
849
928
  return createDoctorCheck({
850
929
  id: "review-workflow-present",
851
930
  label: "Review workflow installed",
852
931
  status: "pass",
853
- message: `${mainWorkflowPath} found`,
932
+ message: mainIsOurs ? `${mainWorkflowPath} found (generated by nx-archweave)` : `${mainWorkflowPath} found, but not generated by nx-archweave (it has no review round marker) \u2014 doctor reports on it, add-review proposes changes to it but never overwrites it`,
854
933
  evidence: [mainWorkflowPath]
855
934
  });
856
935
  }
@@ -970,12 +1049,30 @@ function buildReviewConfigConsistencyCheck(root, mainWorkflowPath, mainExists, c
970
1049
  }
971
1050
 
972
1051
  // ../template-engine/dist/doctor/backend-checks.js
973
- import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync3 } from "node:fs";
1052
+ import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "node:fs";
974
1053
  import { join as join3 } from "node:path";
975
1054
 
976
1055
  // ../template-engine/dist/backend/eslint-boundaries.js
1056
+ var DEFAULT_BOUNDARY_TAGS = { frontend: "scope:frontend", backend: "scope:backend", shared: "scope:shared" };
1057
+ function readBoundaryTags(config) {
1058
+ const boundaries = config?.["boundaries"];
1059
+ const tags = typeof boundaries === "object" && boundaries !== null ? boundaries["tags"] : void 0;
1060
+ const pick = (role) => {
1061
+ const value = typeof tags === "object" && tags !== null ? tags[role] : void 0;
1062
+ return typeof value === "string" && value.trim() !== "" ? value : DEFAULT_BOUNDARY_TAGS[role];
1063
+ };
1064
+ return { frontend: pick("frontend"), backend: pick("backend"), shared: pick("shared") };
1065
+ }
1066
+ function scopeConstraints(tags = DEFAULT_BOUNDARY_TAGS) {
1067
+ return [
1068
+ { sourceTag: tags.frontend, notDependOnLibsWithTags: [tags.backend] },
1069
+ { sourceTag: tags.backend, notDependOnLibsWithTags: [tags.frontend] },
1070
+ { sourceTag: tags.shared, notDependOnLibsWithTags: [tags.frontend, tags.backend] }
1071
+ ];
1072
+ }
1073
+ var SCOPE_CONSTRAINTS = scopeConstraints();
977
1074
  var ESLINT_CONFIG_CANDIDATES = ["eslint.config.mjs", "eslint.config.js", "eslint.config.cjs"];
978
- var APPLIED_MARKER = "sourceTag: 'scope:frontend'";
1075
+ var appliedMarker = (tags) => `sourceTag: '${tags.frontend}'`;
979
1076
  var ALLOW_ALL_BLOCK = /depConstraints:\s*\[\s*\{\s*sourceTag:\s*'\*',\s*onlyDependOnLibsWithTags:\s*\[\s*'\*'\s*\],?\s*\},?\s*\]/g;
980
1077
  function maskComments(text) {
981
1078
  const out = text.split("");
@@ -1028,19 +1125,34 @@ function findAllowAllMatches(masked) {
1028
1125
  }
1029
1126
  return matches;
1030
1127
  }
1031
- function classifyBoundaryConstraints(configText) {
1128
+ function classifyBoundaryConstraints(configText, tags = DEFAULT_BOUNDARY_TAGS) {
1032
1129
  const masked = maskComments(configText);
1033
- if (masked.includes(APPLIED_MARKER))
1130
+ if (masked.includes(appliedMarker(tags)))
1034
1131
  return "enforced";
1035
1132
  if (findAllowAllMatches(masked).length > 0)
1036
1133
  return "allow-all";
1037
1134
  return "unknown";
1038
1135
  }
1039
1136
 
1137
+ // ../templates/dist/fsd/fsd-eslint.js
1138
+ var FSD_ESLINT_MODULE = "@clearlydev/nx-archweave/eslint";
1139
+
1140
+ // ../template-engine/dist/backend/compose-backend-wiring.js
1141
+ var DEFAULT_SHARED_TYPES_ROOT = "libs/shared-types";
1142
+ function readSharedTypesConfig(config) {
1143
+ const value = config?.["sharedTypes"];
1144
+ if (value === false)
1145
+ return false;
1146
+ if (typeof value !== "object" || value === null)
1147
+ return { path: DEFAULT_SHARED_TYPES_ROOT };
1148
+ const record = value;
1149
+ const path = typeof record["path"] === "string" && record["path"].trim() !== "" ? record["path"].replace(/\/+$/, "") : DEFAULT_SHARED_TYPES_ROOT;
1150
+ const importPath = typeof record["importPath"] === "string" && record["importPath"].trim() !== "" ? record["importPath"] : void 0;
1151
+ return importPath === void 0 ? { path } : { path, importPath };
1152
+ }
1153
+
1040
1154
  // ../template-engine/dist/doctor/backend-checks.js
1041
1155
  var CONFIG_PATH2 = "toolkit.config.json";
1042
- var SHARED_TYPES_PACKAGE = "libs/shared-types/package.json";
1043
- var SHARED_TYPES_LEGACY = "libs/shared-types/project.json";
1044
1156
  var BACKEND_CHECK_IDS = [
1045
1157
  "backend-framework",
1046
1158
  "backend-config-consistency",
@@ -1055,11 +1167,13 @@ var CHECK_LABELS = {
1055
1167
  "boundary-constraints-applied": "Module-boundary constraints",
1056
1168
  "boundary-tags-exist": "Boundary tags exist"
1057
1169
  };
1058
- function collectScopeTags(root) {
1170
+ function collectScopeTags(root, projects) {
1171
+ if (projects !== void 0)
1172
+ return new Set(projects.flatMap((project) => project.tags));
1059
1173
  const tags = /* @__PURE__ */ new Set();
1060
1174
  const dirsIn = (rel) => {
1061
1175
  try {
1062
- return readdirSync(join3(root, rel), { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => `${rel}/${entry.name}`);
1176
+ return readdirSync2(join3(root, rel), { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => `${rel}/${entry.name}`);
1063
1177
  } catch {
1064
1178
  return [];
1065
1179
  }
@@ -1098,6 +1212,14 @@ function collectScopeTags(root) {
1098
1212
  function notApplicable(message) {
1099
1213
  return BACKEND_CHECK_IDS.map((id) => createDoctorCheck({ id, label: CHECK_LABELS[id], status: "not-applicable", message, evidence: [] }));
1100
1214
  }
1215
+ function readConfigObject(root) {
1216
+ try {
1217
+ const parsed = JSON.parse(readFileSync3(join3(root, CONFIG_PATH2), "utf-8"));
1218
+ return typeof parsed === "object" && parsed !== null ? parsed : null;
1219
+ } catch {
1220
+ return null;
1221
+ }
1222
+ }
1101
1223
  function readsBackendOnly(root) {
1102
1224
  try {
1103
1225
  return JSON.parse(readFileSync3(join3(root, CONFIG_PATH2), "utf-8")).frontend === "none";
@@ -1121,7 +1243,7 @@ function readConfiguredBackend(root) {
1121
1243
  return null;
1122
1244
  }
1123
1245
  }
1124
- function buildBackendChecks(root) {
1246
+ function buildBackendChecks(root, projects) {
1125
1247
  const backend = readConfiguredBackend(root);
1126
1248
  if (backend === null) {
1127
1249
  return notApplicable("Skipped \u2014 no backend configured in toolkit.config.json");
@@ -1143,21 +1265,21 @@ function buildBackendChecks(root) {
1143
1265
  suggestedAction: "Fix the backend field, then re-run doctor."
1144
1266
  }));
1145
1267
  } else {
1146
- let hasDependency = false;
1268
+ let hasDependency2 = false;
1147
1269
  try {
1148
1270
  const pkg = JSON.parse(readFileSync3(join3(root, "package.json"), "utf-8"));
1149
1271
  const packageName = backend === "nest" ? "@nestjs/core" : "express";
1150
- hasDependency = Boolean(pkg.dependencies?.[packageName] ?? pkg.devDependencies?.[packageName]);
1272
+ hasDependency2 = Boolean(pkg.dependencies?.[packageName] ?? pkg.devDependencies?.[packageName]);
1151
1273
  } catch {
1152
- hasDependency = false;
1274
+ hasDependency2 = false;
1153
1275
  }
1154
1276
  checks.push(createDoctorCheck({
1155
1277
  id: "backend-framework",
1156
1278
  label: CHECK_LABELS["backend-framework"],
1157
- status: hasDependency ? "pass" : "warning",
1158
- message: hasDependency ? `${backend} dependency detected in the root package.json` : `backend is configured as ${backend} but its dependency was not found in the root package.json`,
1159
- evidence: hasDependency ? ["package.json"] : [],
1160
- suggestedAction: hasDependency ? void 0 : "Run the official generator commands shown by add-backend."
1279
+ status: hasDependency2 ? "pass" : "warning",
1280
+ message: hasDependency2 ? `${backend} dependency detected in the root package.json` : `backend is configured as ${backend} but its dependency was not found in the root package.json`,
1281
+ evidence: hasDependency2 ? ["package.json"] : [],
1282
+ suggestedAction: hasDependency2 ? void 0 : "Run the official generator commands shown by add-backend."
1161
1283
  }), createDoctorCheck({
1162
1284
  id: "backend-config-consistency",
1163
1285
  label: CHECK_LABELS["backend-config-consistency"],
@@ -1166,23 +1288,37 @@ function buildBackendChecks(root) {
1166
1288
  evidence: [CONFIG_PATH2]
1167
1289
  }));
1168
1290
  }
1169
- const sharedTypesCurrent = existsSync3(join3(root, SHARED_TYPES_PACKAGE));
1170
- const sharedTypesLegacy = !sharedTypesCurrent && existsSync3(join3(root, SHARED_TYPES_LEGACY));
1171
- const sharedTypesExists = sharedTypesCurrent || sharedTypesLegacy;
1172
- checks.push(createDoctorCheck({
1173
- id: "shared-types-lib",
1174
- label: CHECK_LABELS["shared-types-lib"],
1175
- status: sharedTypesExists ? "pass" : "warning",
1176
- message: sharedTypesCurrent ? "libs/shared-types present" : sharedTypesLegacy ? "libs/shared-types present (legacy project.json shape)" : "libs/shared-types not found \u2014 run add-backend to wire it",
1177
- evidence: sharedTypesCurrent ? [SHARED_TYPES_PACKAGE] : sharedTypesLegacy ? [SHARED_TYPES_LEGACY] : []
1178
- }));
1291
+ const config = readConfigObject(root);
1292
+ const sharedTypesConfig = readSharedTypesConfig(config);
1293
+ if (sharedTypesConfig === false) {
1294
+ checks.push(createDoctorCheck({
1295
+ id: "shared-types-lib",
1296
+ label: CHECK_LABELS["shared-types-lib"],
1297
+ status: "not-applicable",
1298
+ message: "no shared-types lib (sharedTypes: false in toolkit.config.json)",
1299
+ evidence: [CONFIG_PATH2]
1300
+ }));
1301
+ } else {
1302
+ const current = `${sharedTypesConfig.path}/package.json`;
1303
+ const legacy = `${sharedTypesConfig.path}/project.json`;
1304
+ const sharedTypesCurrent = existsSync3(join3(root, current));
1305
+ const sharedTypesLegacy = !sharedTypesCurrent && existsSync3(join3(root, legacy));
1306
+ checks.push(createDoctorCheck({
1307
+ id: "shared-types-lib",
1308
+ label: CHECK_LABELS["shared-types-lib"],
1309
+ status: sharedTypesCurrent || sharedTypesLegacy ? "pass" : "warning",
1310
+ message: sharedTypesCurrent ? `${sharedTypesConfig.path} present` : sharedTypesLegacy ? `${sharedTypesConfig.path} present (legacy project.json shape)` : `${sharedTypesConfig.path} not found \u2014 run add-backend to wire it`,
1311
+ evidence: sharedTypesCurrent ? [current] : sharedTypesLegacy ? [legacy] : []
1312
+ }));
1313
+ }
1314
+ const boundaryTags = readBoundaryTags(config);
1179
1315
  const eslintPath = ESLINT_CONFIG_CANDIDATES.find((candidate) => existsSync3(join3(root, candidate)));
1180
- const classification = eslintPath ? classifyBoundaryConstraints(readFileSync3(join3(root, eslintPath), "utf-8")) : "unknown";
1316
+ const classification = eslintPath ? classifyBoundaryConstraints(readFileSync3(join3(root, eslintPath), "utf-8"), boundaryTags) : "unknown";
1181
1317
  checks.push(createDoctorCheck({
1182
1318
  id: "boundary-constraints-applied",
1183
1319
  label: CHECK_LABELS["boundary-constraints-applied"],
1184
1320
  status: classification === "enforced" ? "pass" : "warning",
1185
- message: classification === "enforced" ? "scope depConstraints are enforced" : classification === "allow-all" ? "ESLint still has the allow-all default \u2014 boundaries are NOT enforced" : "could not verify the ESLint config (missing or unrecognized shape)",
1321
+ message: classification === "enforced" ? `${boundaryTags.frontend} / ${boundaryTags.backend} depConstraints are enforced` : classification === "allow-all" ? "ESLint still has the allow-all default \u2014 boundaries are NOT enforced" : "could not verify the ESLint config (missing or unrecognized shape)",
1186
1322
  evidence: eslintPath ? [eslintPath] : [],
1187
1323
  suggestedAction: classification === "enforced" ? void 0 : "Run add-backend with --applyEslint, or merge its snippet yourself."
1188
1324
  }));
@@ -1195,41 +1331,46 @@ function buildBackendChecks(root) {
1195
1331
  evidence: []
1196
1332
  }));
1197
1333
  } else {
1198
- const tags = collectScopeTags(root);
1199
- const required = readsBackendOnly(root) ? ["scope:backend"] : ["scope:frontend", "scope:backend"];
1334
+ const tags = collectScopeTags(root, projects);
1335
+ const required = readsBackendOnly(root) ? [boundaryTags.backend] : [boundaryTags.frontend, boundaryTags.backend];
1200
1336
  const missing = required.filter((tag) => !tags.has(tag));
1201
1337
  checks.push(createDoctorCheck({
1202
1338
  id: "boundary-tags-exist",
1203
1339
  label: CHECK_LABELS["boundary-tags-exist"],
1204
1340
  status: missing.length === 0 ? "pass" : "warning",
1205
- message: missing.length === 0 ? required.length === 1 ? "scope:backend is carried by at least one project (backend-only workspace \u2014 no frontend to tag)" : "both scope:frontend and scope:backend are carried by at least one project" : `the toolkit's enforced deny rules reference ${missing.join(" and ")}, but no project in the scanned directories (apps/, libs/, packages/) carries ${missing.length === 1 ? "it" : "them"} \u2014 the boundary is a dead letter`,
1341
+ message: missing.length === 0 ? required.length === 1 ? `${boundaryTags.backend} is carried by at least one project (backend-only workspace \u2014 no frontend to tag)` : `both ${boundaryTags.frontend} and ${boundaryTags.backend} are carried by at least one project` : `the toolkit's enforced deny rules reference ${missing.join(" and ")}, but no project ${projects === void 0 ? "in the scanned directories (apps/, libs/, packages/) " : ""}carries ${missing.length === 1 ? "it" : "them"} \u2014 the boundary is a dead letter`,
1206
1342
  evidence: missing.length === 0 ? [] : missing,
1207
- suggestedAction: missing.length === 0 ? void 0 : "Add the missing scope tag(s) to the app project.json tags or package.json nx.tags (add-backend does this for the two apps)."
1343
+ suggestedAction: missing.length === 0 ? void 0 : "Add the missing tag(s) to the app project.json tags or package.json nx.tags (add-backend does this for the two apps)."
1208
1344
  }));
1209
1345
  }
1210
1346
  return checks;
1211
1347
  }
1212
1348
 
1213
1349
  // ../template-engine/dist/doctor/fsd-like-check.js
1214
- import { existsSync as existsSync4, readdirSync as readdirSync2 } from "node:fs";
1350
+ import { existsSync as existsSync4, readdirSync as readdirSync3 } from "node:fs";
1215
1351
  import { join as join4 } from "node:path";
1216
1352
  var FSD_LAYERS = ["app", "_app", "pages", "_pages", "widgets", "features", "entities", "shared"];
1217
- function buildFsdLikeCheck(root) {
1353
+ function conventionalSourceRoots(root) {
1218
1354
  const scanRoots = ["src"];
1219
1355
  try {
1220
- for (const entry of readdirSync2(join4(root, "apps"), { withFileTypes: true })) {
1356
+ for (const entry of readdirSync3(join4(root, "apps"), { withFileTypes: true })) {
1221
1357
  if (entry.isDirectory())
1222
1358
  scanRoots.push(`apps/${entry.name}/src`);
1223
1359
  }
1224
1360
  } catch {
1225
1361
  }
1226
- const found = [];
1362
+ return scanRoots;
1363
+ }
1364
+ function buildFsdLikeCheck(root, projects) {
1365
+ const scanRoots = projects === void 0 ? conventionalSourceRoots(root) : [...new Set(projects.map((project) => project.sourceRoot))].sort();
1366
+ let found = [];
1227
1367
  for (const scanRoot of scanRoots) {
1228
1368
  for (const layer of FSD_LAYERS) {
1229
1369
  if (existsSync4(join4(root, scanRoot, layer)))
1230
- found.push(`${scanRoot}/${layer}`);
1370
+ found.push(scanRoot === "." || scanRoot === "" ? layer : `${scanRoot}/${layer}`);
1231
1371
  }
1232
1372
  }
1373
+ found = found.sort();
1233
1374
  if (existsSync4(join4(root, "toolkit.config.json"))) {
1234
1375
  return createDoctorCheck({
1235
1376
  id: "fsd-like-structure",
@@ -1259,17 +1400,102 @@ function buildFsdLikeCheck(root) {
1259
1400
  }
1260
1401
 
1261
1402
  // ../template-engine/dist/doctor/fsd-enforcement-check.js
1262
- import { existsSync as existsSync5, readdirSync as readdirSync3, readFileSync as readFileSync4 } from "node:fs";
1403
+ import { existsSync as existsSync5, readdirSync as readdirSync4, readFileSync as readFileSync4 } from "node:fs";
1263
1404
  import { join as join5 } from "node:path";
1264
1405
 
1265
- // ../templates/dist/fsd/fsd-eslint.js
1266
- var FSD_ESLINT_MODULE = "@clearlydev/nx-archweave/eslint";
1406
+ // ../template-engine/dist/frameworks/profiles.js
1407
+ var ALL_LAYERS = ["app", "pages", "features", "entities", "shared"];
1408
+ var NEXT = {
1409
+ id: "next",
1410
+ recognised: true,
1411
+ // Next.js's own app/ (App Router) directory is the composition root; FSD's Next.js guide
1412
+ // names the FSD app and pages layers _app and _pages, since Next.js reads src/app and
1413
+ // src/pages as its routers.
1414
+ layers: ["pages", "features", "entities", "shared"],
1415
+ layerDirs: { app: "_app", pages: "_pages" },
1416
+ buildOutputDirs: [".next", "out"],
1417
+ platformSuffixes: [],
1418
+ routerDirs: ["pages"]
1419
+ };
1420
+ var REACT_VITE = {
1421
+ id: "react-vite",
1422
+ recognised: true,
1423
+ layers: ALL_LAYERS,
1424
+ layerDirs: {},
1425
+ buildOutputDirs: ["dist"],
1426
+ platformSuffixes: [],
1427
+ routerDirs: []
1428
+ };
1429
+ var EXPO_ROUTER = {
1430
+ id: "expo",
1431
+ recognised: true,
1432
+ layers: ALL_LAYERS,
1433
+ layerDirs: { app: "_app" },
1434
+ buildOutputDirs: [".expo", "dist"],
1435
+ platformSuffixes: ["ios", "android", "native", "web"],
1436
+ routerDirs: []
1437
+ };
1438
+ var EXPO = { ...EXPO_ROUTER, layerDirs: {} };
1439
+ var GENERIC = {
1440
+ id: "generic",
1441
+ recognised: false,
1442
+ layers: ALL_LAYERS,
1443
+ layerDirs: {},
1444
+ buildOutputDirs: ["dist", "build"],
1445
+ platformSuffixes: [],
1446
+ routerDirs: []
1447
+ };
1448
+ var NEXT_CONFIGS = ["next.config.js", "next.config.mjs", "next.config.ts", "next.config.cjs"];
1449
+ var VITE_CONFIGS = ["vite.config.ts", "vite.config.mts", "vite.config.js", "vite.config.mjs"];
1450
+ var EXPO_CONFIGS = ["app.config.ts", "app.config.js", "app.config.mjs"];
1451
+ function hasExpoKey(input, file) {
1452
+ const text = input.read(file);
1453
+ if (typeof text !== "string")
1454
+ return false;
1455
+ try {
1456
+ return typeof JSON.parse(text)["expo"] === "object";
1457
+ } catch {
1458
+ return false;
1459
+ }
1460
+ }
1461
+ function dependsOn(input, manifest, name) {
1462
+ const text = input.read(manifest);
1463
+ if (typeof text !== "string")
1464
+ return false;
1465
+ try {
1466
+ const parsed = JSON.parse(text);
1467
+ return ["dependencies", "devDependencies", "peerDependencies"].some((field) => parsed[field]?.[name] !== void 0);
1468
+ } catch {
1469
+ return false;
1470
+ }
1471
+ }
1472
+ function detectFramework(input) {
1473
+ const prefix = input.projectRoot === "" || input.projectRoot === "." ? "" : `${input.projectRoot}/`;
1474
+ const executors = input.executors ?? [];
1475
+ if (NEXT_CONFIGS.some((file) => input.exists(`${prefix}${file}`)) || executors.some((executor) => executor.startsWith("@nx/next:"))) {
1476
+ return NEXT;
1477
+ }
1478
+ const dependsAnywhere = (name) => dependsOn(input, `${prefix}package.json`, name) || dependsOn(input, "package.json", name);
1479
+ 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");
1480
+ if (usesExpo)
1481
+ return dependsAnywhere("expo-router") ? EXPO_ROUTER : EXPO;
1482
+ const usesReact = dependsAnywhere("react");
1483
+ const usesVite = VITE_CONFIGS.some((file) => input.exists(`${prefix}${file}`)) || executors.some((executor) => executor.startsWith("@nx/vite:"));
1484
+ if (usesVite && usesReact)
1485
+ return REACT_VITE;
1486
+ return GENERIC;
1487
+ }
1267
1488
 
1268
1489
  // ../template-engine/dist/fsd/fsd-eslint-config.js
1269
1490
  function classifyFsdEslintConfig(configText) {
1270
1491
  return maskComments(configText).includes(FSD_ESLINT_MODULE) ? "applied" : "absent";
1271
1492
  }
1272
1493
 
1494
+ // ../template-engine/dist/doctor/doctor-projects.js
1495
+ function projectPrefix(root) {
1496
+ return root === "" || root === "." ? "" : `${root}/`;
1497
+ }
1498
+
1273
1499
  // ../template-engine/dist/doctor/fsd-enforcement-check.js
1274
1500
  function fsdEnabledPerConfig(root) {
1275
1501
  const path = join5(root, "toolkit.config.json");
@@ -1286,10 +1512,14 @@ function fsdEnabledPerConfig(root) {
1286
1512
  return null;
1287
1513
  }
1288
1514
  }
1289
- function eslintConfigPaths(root) {
1515
+ function eslintConfigPaths(root, projects) {
1516
+ if (projects !== void 0) {
1517
+ const dirs2 = ["", ...projects.map((project) => projectPrefix(project.root))];
1518
+ return [...new Set(dirs2.flatMap((dir) => ESLINT_CONFIG_CANDIDATES.map((name) => `${dir}${name}`)))].filter((rel) => existsSync5(join5(root, rel)));
1519
+ }
1290
1520
  const dirsIn = (rel) => {
1291
1521
  try {
1292
- return readdirSync3(join5(root, rel), { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => rel === "" ? entry.name : `${rel}/${entry.name}`);
1522
+ return readdirSync4(join5(root, rel), { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => rel === "" ? entry.name : `${rel}/${entry.name}`);
1293
1523
  } catch {
1294
1524
  return [];
1295
1525
  }
@@ -1298,7 +1528,7 @@ function eslintConfigPaths(root) {
1298
1528
  const dirs = ["", ...apps, ...apps.flatMap((app) => dirsIn(app))];
1299
1529
  return dirs.flatMap((dir) => ESLINT_CONFIG_CANDIDATES.map((name) => dir === "" ? name : `${dir}/${name}`)).filter((rel) => existsSync5(join5(root, rel)));
1300
1530
  }
1301
- function buildFsdEnforcementCheck(root) {
1531
+ function buildFsdEnforcementCheck(root, projects) {
1302
1532
  const id = "fsd-enforcement-applied";
1303
1533
  const label = "FSD layer rule applied";
1304
1534
  const enabled = fsdEnabledPerConfig(root);
@@ -1311,7 +1541,7 @@ function buildFsdEnforcementCheck(root) {
1311
1541
  evidence: []
1312
1542
  });
1313
1543
  }
1314
- const applied = eslintConfigPaths(root).filter((rel) => classifyFsdEslintConfig(readFileSync4(join5(root, rel), "utf-8")) === "applied");
1544
+ const applied = appliedFsdConfigs(root, projects);
1315
1545
  if (applied.length > 0) {
1316
1546
  return createDoctorCheck({ id, label, status: "pass", message: "the archweave/fsd-boundaries rule is wired into an ESLint config", evidence: applied });
1317
1547
  }
@@ -1324,11 +1554,35 @@ function buildFsdEnforcementCheck(root) {
1324
1554
  suggestedAction: "Run add-fsd-lite --applyEslint (or paste the snippet it prints into the frontend app's eslint.config.mjs)."
1325
1555
  });
1326
1556
  }
1557
+ function appliedFsdConfigs(root, projects) {
1558
+ return eslintConfigPaths(root, projects).filter((rel) => classifyFsdEslintConfig(readFileSync4(join5(root, rel), "utf-8")) === "applied");
1559
+ }
1560
+ function buildFsdRuleAnchoredCheck(root, projects) {
1561
+ const id = "fsd-rule-anchored";
1562
+ const label = "FSD rule anchored to its config";
1563
+ const applied = appliedFsdConfigs(root, projects);
1564
+ if (applied.length === 0) {
1565
+ return createDoctorCheck({ id, label, status: "not-applicable", message: "no ESLint config wires the FSD rule", evidence: [] });
1566
+ }
1567
+ const unanchored = applied.filter((rel) => !/fsdBoundaries\(\s*\{[^}]*\bcwd\s*:/s.test(maskComments(readFileSync4(join5(root, rel), "utf-8"))));
1568
+ if (unanchored.length === 0) {
1569
+ return createDoctorCheck({ id, label, status: "pass", message: "every FSD rule config passes cwd", evidence: applied });
1570
+ }
1571
+ return createDoctorCheck({
1572
+ id,
1573
+ label,
1574
+ status: "warning",
1575
+ message: "fsdBoundaries() is called without cwd \u2014 run from the workspace root or an editor, the rule checks no file",
1576
+ evidence: unanchored,
1577
+ suggestedAction: "Pass the config's own directory: fsdBoundaries({ severity, cwd: import.meta.dirname, root: '<source root>' })."
1578
+ });
1579
+ }
1327
1580
 
1328
1581
  // ../template-engine/dist/doctor/run-doctor-checks.js
1329
1582
  var NOT_APPLICABLE_IDS = [
1330
1583
  "fsd-like-structure",
1331
1584
  "fsd-enforcement-applied",
1585
+ "fsd-rule-anchored",
1332
1586
  "nx-version",
1333
1587
  "package-manager",
1334
1588
  "workspace-root",
@@ -1341,7 +1595,7 @@ var NOT_APPLICABLE_IDS = [
1341
1595
  ...REVIEW_CHECK_IDS,
1342
1596
  ...BACKEND_CHECK_IDS
1343
1597
  ];
1344
- function runDoctorChecks(root) {
1598
+ function runDoctorChecks(root, projects) {
1345
1599
  const env = detectEnvironment(root);
1346
1600
  if (!env.hasNxWorkspace) {
1347
1601
  const checks2 = [
@@ -1372,8 +1626,9 @@ function runDoctorChecks(root) {
1372
1626
  evidence: ["nx.json"]
1373
1627
  }),
1374
1628
  buildNxVersionCheck(env),
1375
- buildFsdLikeCheck(root),
1376
- buildFsdEnforcementCheck(root),
1629
+ buildFsdLikeCheck(root, projects),
1630
+ buildFsdEnforcementCheck(root, projects),
1631
+ buildFsdRuleAnchoredCheck(root, projects),
1377
1632
  createDoctorCheck({
1378
1633
  id: "package-manager",
1379
1634
  label: "Package manager",
@@ -1394,7 +1649,7 @@ function runDoctorChecks(root) {
1394
1649
  buildGithubTemplatesCheck(root),
1395
1650
  buildIntegrationManifestCheck(root),
1396
1651
  ...buildReviewChecks(root),
1397
- ...buildBackendChecks(root)
1652
+ ...buildBackendChecks(root, projects)
1398
1653
  ];
1399
1654
  const overallStatus = computeOverallStatus(checks);
1400
1655
  return { checks, overallStatus };
@@ -2336,6 +2591,22 @@ import { join as join10 } from "node:path";
2336
2591
 
2337
2592
  // src/generators/shared/toolkit-config-file.ts
2338
2593
  var TOOLKIT_CONFIG_PATH = "toolkit.config.json";
2594
+ function readToolkitConfigFile(tree) {
2595
+ const raw = tree.read(TOOLKIT_CONFIG_PATH, "utf-8");
2596
+ if (raw === null) return { raw: null, parsed: null };
2597
+ try {
2598
+ const parsed = JSON.parse(raw);
2599
+ return {
2600
+ raw,
2601
+ parsed: typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : null
2602
+ };
2603
+ } catch {
2604
+ return { raw, parsed: null };
2605
+ }
2606
+ }
2607
+ function readToolkitConfig(tree) {
2608
+ return readToolkitConfigFile(tree).parsed;
2609
+ }
2339
2610
  function recordedChoice(config, key, allowed) {
2340
2611
  const value = config?.[key];
2341
2612
  return typeof value === "string" && allowed.includes(value) ? value : void 0;
@@ -2344,6 +2615,13 @@ var RECORDED_PACKAGE_MANAGERS = ["pnpm", "npm", "yarn", "bun"];
2344
2615
  function workspacePackageManager(exists, read, config) {
2345
2616
  return detectWorkspacePackageManager(exists, read) ?? recordedChoice(config, "packageManager", RECORDED_PACKAGE_MANAGERS) ?? null;
2346
2617
  }
2618
+ function recordedFrontends(config) {
2619
+ const frontends = config?.["frontends"];
2620
+ if (!Array.isArray(frontends)) return [];
2621
+ return frontends.flatMap(
2622
+ (entry) => typeof entry === "object" && entry !== null && typeof entry["project"] === "string" ? [entry.project] : []
2623
+ );
2624
+ }
2347
2625
 
2348
2626
  // src/generators/shared/run-nx.ts
2349
2627
  function runNxFor(exists, read) {
@@ -2366,7 +2644,7 @@ function diskRunNx(root) {
2366
2644
  var DOCTOR_TARGET_ENVIRONMENT = "claude-code";
2367
2645
  async function buildDoctorReportWithIntegrations(root, options = {}, deps = {}) {
2368
2646
  const integrationsRoot = deps.integrationsRoot ?? createIntegrationsCompositionRoot();
2369
- const baseReport = runDoctorChecks(root);
2647
+ const baseReport = runDoctorChecks(root, options.projects);
2370
2648
  const declaredIds = readEnabledIntegrationIds(root);
2371
2649
  const runNx = diskRunNx(root);
2372
2650
  const driftResults = await detectIntegrationsDrift({
@@ -2412,11 +2690,417 @@ async function applyLiveVerification(driftResults, ctx) {
2412
2690
  return driftResults.map((result) => live.get(result.id) ?? result);
2413
2691
  }
2414
2692
 
2693
+ // src/generators/doctor/workspace-checks.ts
2694
+ import { getProjects as getProjects2, joinPathFragments } from "@nx/devkit";
2695
+
2696
+ // src/eslint/fsd-policy.ts
2697
+ var PRESETS = {
2698
+ basic: {
2699
+ sliceGroups: { auto: true, include: [], exclude: [] },
2700
+ sharedPublicApi: "segment",
2701
+ testFiles: false,
2702
+ segments: [],
2703
+ platformSuffixes: [],
2704
+ routerDirs: []
2705
+ },
2706
+ practical: {
2707
+ sliceGroups: { auto: true, include: [], exclude: [] },
2708
+ sharedPublicApi: "file",
2709
+ testFiles: ["**/*.{spec,test}.*", "**/__tests__/**"],
2710
+ segments: [],
2711
+ platformSuffixes: [],
2712
+ routerDirs: []
2713
+ }
2714
+ };
2715
+ var isStringArray = (value) => Array.isArray(value) && value.every((item) => typeof item === "string");
2716
+ function resolveFsdPolicy(recorded, overrides = {}) {
2717
+ const fsd = typeof recorded === "object" && recorded !== null ? recorded : {};
2718
+ const preset = fsd["preset"] === "practical" ? "practical" : "basic";
2719
+ const policy = { ...PRESETS[preset] };
2720
+ const groups = fsd["sliceGroups"];
2721
+ if (groups === false) policy.sliceGroups = false;
2722
+ else if (groups === "auto") policy.sliceGroups = { auto: true, include: [], exclude: [] };
2723
+ else if (typeof groups === "object" && groups !== null) {
2724
+ const value = groups;
2725
+ policy.sliceGroups = {
2726
+ auto: value["auto"] !== false,
2727
+ include: isStringArray(value["include"]) ? value["include"] : [],
2728
+ exclude: isStringArray(value["exclude"]) ? value["exclude"] : []
2729
+ };
2730
+ }
2731
+ const shared = fsd["sharedPublicApi"];
2732
+ if (shared === "segment" || shared === "component" || shared === "file") policy.sharedPublicApi = shared;
2733
+ const tests = fsd["testFiles"];
2734
+ if (tests === false || isStringArray(tests)) policy.testFiles = tests;
2735
+ if (isStringArray(fsd["segments"])) policy.segments = fsd["segments"];
2736
+ if (isStringArray(fsd["platformSuffixes"])) policy.platformSuffixes = fsd["platformSuffixes"];
2737
+ if (isStringArray(fsd["routerDirs"])) policy.routerDirs = fsd["routerDirs"];
2738
+ for (const [key, value] of Object.entries(overrides)) {
2739
+ if (value !== void 0) policy[key] = value;
2740
+ }
2741
+ return policy;
2742
+ }
2743
+
2744
+ // src/eslint/slice-groups.ts
2745
+ import { readdirSync as readdirSync5, statSync as statSync2 } from "node:fs";
2746
+ import { join as join11 } from "node:path";
2747
+ var SEGMENTS = [
2748
+ "ui",
2749
+ "model",
2750
+ "api",
2751
+ "lib",
2752
+ "config",
2753
+ "components",
2754
+ "hooks",
2755
+ "types",
2756
+ "utils",
2757
+ "helpers",
2758
+ "constants",
2759
+ "consts",
2760
+ "store",
2761
+ "stores",
2762
+ "styles",
2763
+ "assets",
2764
+ "services",
2765
+ "context"
2766
+ ];
2767
+ var IGNORED = /^(\.|readme(\.|$))/i;
2768
+ var listings = /* @__PURE__ */ new Map();
2769
+ function list(dir) {
2770
+ let mtimeMs;
2771
+ try {
2772
+ mtimeMs = statSync2(dir).mtimeMs;
2773
+ } catch {
2774
+ listings.delete(dir);
2775
+ return null;
2776
+ }
2777
+ const cached = listings.get(dir);
2778
+ if (cached !== void 0 && cached.mtimeMs === mtimeMs) return cached.entries;
2779
+ try {
2780
+ const entries = readdirSync5(dir, { withFileTypes: true }).map((entry) => ({ name: entry.name, directory: entry.isDirectory() }));
2781
+ listings.set(dir, { mtimeMs, entries });
2782
+ return entries;
2783
+ } catch {
2784
+ return null;
2785
+ }
2786
+ }
2787
+ function createGroupDetector(rootAbsolute, policy, isIndexFile, listDirectory = list) {
2788
+ const groups = policy.sliceGroups;
2789
+ if (groups === false) return () => false;
2790
+ const segments = /* @__PURE__ */ new Set([...SEGMENTS, ...policy.segments]);
2791
+ return (layerDir, name) => {
2792
+ const key = `${layerDir}/${name}`;
2793
+ if (groups.include.includes(key)) return true;
2794
+ if (groups.exclude.includes(key) || !groups.auto) return false;
2795
+ const entries = listDirectory(join11(rootAbsolute, layerDir, name))?.filter((entry) => !IGNORED.test(entry.name)) ?? null;
2796
+ if (entries === null || entries.length === 0) return false;
2797
+ if (entries.some((entry) => !entry.directory || segments.has(entry.name))) return false;
2798
+ return entries.some((entry) => listDirectory(join11(rootAbsolute, layerDir, name, entry.name))?.some((child) => !child.directory && isIndexFile(child.name)) ?? false);
2799
+ };
2800
+ }
2801
+
2802
+ // src/eslint/fsd-boundaries.ts
2803
+ function indexFilePattern(platformSuffixes) {
2804
+ const platforms = platformSuffixes.length === 0 ? "" : `(\\.(${platformSuffixes.map((suffix) => suffix.replace(/[^a-z0-9-]/gi, "")).join("|")}))?`;
2805
+ return new RegExp(`^index${platforms}(\\.[cm]?[jt]sx?)?$`);
2806
+ }
2807
+ var DEFAULT_POLICY = resolveFsdPolicy(void 0);
2808
+
2809
+ // src/generators/shared/frontend-app.ts
2810
+ import { getProjects } from "@nx/devkit";
2811
+
2812
+ // src/generators/shared/project-type.ts
2813
+ import { parseJson } from "@nx/devkit";
2814
+ function resolveProjectType(tree, root, declared) {
2815
+ if (declared) return declared === "library" ? "library" : "application";
2816
+ const prefix = root === "" || root === "." ? "" : `${root}/`;
2817
+ if (tree.exists(`${prefix}tsconfig.lib.json`)) return "library";
2818
+ if (tree.exists(`${prefix}tsconfig.app.json`)) return "application";
2819
+ const raw = tree.read(`${prefix}package.json`, "utf-8");
2820
+ if (raw === null) return "library";
2821
+ try {
2822
+ const manifest = parseJson(raw);
2823
+ if (manifest && !["exports", "main", "module", "bin"].some((key) => Boolean(manifest[key]))) return "application";
2824
+ } catch {
2825
+ }
2826
+ return "library";
2827
+ }
2828
+
2829
+ // src/generators/shared/frontend-app.ts
2830
+ var baseNameOf = (root) => root.split("/").pop() ?? root;
2831
+ function isEndToEnd(root) {
2832
+ const dir = baseNameOf(root);
2833
+ return dir === "e2e" || dir.endsWith("-e2e");
2834
+ }
2835
+ function isBackend(root, config, backendTag) {
2836
+ return baseNameOf(root) === "api" || (config.tags ?? []).includes(backendTag);
2837
+ }
2838
+ function prefixOf(root) {
2839
+ return root === "" || root === "." ? "" : `${root}/`;
2840
+ }
2841
+ function sourceRootOf(tree, config) {
2842
+ const prefix = prefixOf(config.root);
2843
+ const declared = config.sourceRoot?.replace(/\/$/, "");
2844
+ const atRoot = declared !== void 0 && (declared === config.root || prefix === "" && (declared === "." || declared === ""));
2845
+ if (declared && !atRoot) return declared;
2846
+ if (atRoot && !tree.exists(`${prefix}src`)) return prefix === "" ? "." : config.root;
2847
+ return `${prefix}src`;
2848
+ }
2849
+ function frameworkOf(tree, config) {
2850
+ return detectFramework({
2851
+ exists: (path) => tree.exists(path),
2852
+ read: (path) => tree.read(path, "utf-8"),
2853
+ projectRoot: config.root,
2854
+ executors: Object.values(config.targets ?? {}).flatMap((target) => target.executor ? [target.executor] : [])
2855
+ });
2856
+ }
2857
+ function resolveFrontendApp(tree, explicitProject) {
2858
+ let projects;
2859
+ try {
2860
+ projects = getProjects(tree);
2861
+ } catch (error) {
2862
+ const message = error instanceof Error ? error.message : String(error);
2863
+ const pathMatch = /Cannot parse ([^:]+):/.exec(message);
2864
+ return { kind: "invalid-project-json", detail: pathMatch ? pathMatch[1] : message };
2865
+ }
2866
+ const found = (project2, config2) => {
2867
+ const profile = frameworkOf(tree, config2);
2868
+ return { kind: "found", project: project2, root: config2.root, sourceRoot: sourceRootOf(tree, config2), framework: profile.id, profile };
2869
+ };
2870
+ if (explicitProject !== void 0) {
2871
+ const config2 = projects.get(explicitProject);
2872
+ if (config2) return found(explicitProject, config2);
2873
+ const rootApp = inferredRootApp(tree, projects);
2874
+ return rootApp?.kind === "found" && rootApp.project === explicitProject ? rootApp : { kind: "not-found", project: explicitProject };
2875
+ }
2876
+ const backendTag = readBoundaryTags(readToolkitConfig(tree)).backend;
2877
+ const eligible = [...projects.entries()].filter(([, config2]) => !isEndToEnd(config2.root) && !isBackend(config2.root, config2, backendTag));
2878
+ const declared = eligible.filter(([, config2]) => config2.projectType === "application");
2879
+ const inferred = eligible.filter(([, config2]) => {
2880
+ if (config2.projectType !== void 0) return false;
2881
+ if (config2.root === "" || config2.root === ".") return frameworkOf(tree, config2).recognised;
2882
+ return resolveProjectType(tree, config2.root, void 0) === "application";
2883
+ });
2884
+ const candidates = (declared.length > 0 ? declared : inferred).sort(([a], [b]) => a.localeCompare(b));
2885
+ if (candidates.length === 0) {
2886
+ const rootApp = inferredRootApp(tree, projects);
2887
+ if (rootApp !== null) return rootApp;
2888
+ const otherProjects = [...projects.keys()].filter((name) => !isEndToEnd(projects.get(name)?.root ?? "")).sort();
2889
+ return { kind: "none", otherProjects };
2890
+ }
2891
+ if (candidates.length > 1) return { kind: "ambiguous", candidates: candidates.map(([name]) => name) };
2892
+ const [project, config] = candidates[0];
2893
+ return found(project, config);
2894
+ }
2895
+ function inferredRootApp(tree, projects) {
2896
+ if ([...projects.values()].some((config2) => config2.root === "" || config2.root === ".")) return null;
2897
+ const raw = tree.read("package.json", "utf-8");
2898
+ if (raw === null) return null;
2899
+ let name;
2900
+ try {
2901
+ name = JSON.parse(raw).name;
2902
+ } catch {
2903
+ return null;
2904
+ }
2905
+ if (typeof name !== "string" || name === "") return null;
2906
+ const config = { name, root: "." };
2907
+ const framework = frameworkOf(tree, config);
2908
+ if (!framework.recognised) return null;
2909
+ return { kind: "found", project: name, root: ".", sourceRoot: sourceRootOf(tree, config), framework: framework.id, profile: framework, inferredRoot: true };
2910
+ }
2911
+
2912
+ // src/generators/slice/core.ts
2913
+ function hasDependency(tree, manifest, name) {
2914
+ const raw = tree.read(manifest, "utf-8");
2915
+ if (raw === null) return false;
2916
+ try {
2917
+ const parsed = JSON.parse(raw);
2918
+ return parsed["dependencies"]?.[name] !== void 0 || parsed["devDependencies"]?.[name] !== void 0;
2919
+ } catch {
2920
+ return false;
2921
+ }
2922
+ }
2923
+ function detectUnitTestRunner(tree, projectRoot) {
2924
+ const prefix = projectRoot === "" || projectRoot === "." ? "" : `${projectRoot}/`;
2925
+ const project = tree.read(`${prefix}project.json`, "utf-8");
2926
+ if (project !== null) {
2927
+ try {
2928
+ const targets = JSON.parse(project).targets ?? {};
2929
+ for (const target of Object.values(targets)) {
2930
+ if (target?.executor?.startsWith("@nx/jest:")) return "jest";
2931
+ if (target?.executor?.startsWith("@nx/vite:test") || target?.executor?.startsWith("@nx/vitest:")) return "vitest";
2932
+ }
2933
+ } catch {
2934
+ }
2935
+ }
2936
+ for (const dir of prefix === "" ? [""] : [prefix, ""]) {
2937
+ if (["ts", "mts", "cts", "js", "mjs", "cjs"].some((ext) => tree.exists(`${dir}jest.config.${ext}`))) return "jest";
2938
+ if (["ts", "mts", "js", "mjs"].some((ext) => tree.exists(`${dir}vitest.config.${ext}`) || tree.exists(`${dir}vitest.workspace.${ext}`))) return "vitest";
2939
+ for (const ext of ["ts", "mts", "js", "mjs"]) {
2940
+ const vite = tree.read(`${dir}vite.config.${ext}`, "utf-8");
2941
+ if (vite !== null && /\btest\s*:/.test(vite)) return "vitest";
2942
+ }
2943
+ }
2944
+ for (const manifest of prefix === "" ? ["package.json"] : [`${prefix}package.json`, "package.json"]) {
2945
+ if (hasDependency(tree, manifest, "vitest")) return "vitest";
2946
+ if (hasDependency(tree, manifest, "jest")) return "jest";
2947
+ }
2948
+ return "none";
2949
+ }
2950
+
2951
+ // src/generators/doctor/workspace-checks.ts
2952
+ function doctorProjects(tree) {
2953
+ try {
2954
+ return [...getProjects2(tree).entries()].map(([name, config]) => ({ name, root: config.root, sourceRoot: sourceRootOf(tree, config), tags: config.tags ?? [] })).sort((a, b) => a.name.localeCompare(b.name));
2955
+ } catch {
2956
+ return void 0;
2957
+ }
2958
+ }
2959
+ var SLICED_LAYER_DIRS = ["pages", "_pages", "widgets", "features", "entities"];
2960
+ var SOURCE_FILE = /\.(?:[cm]?[jt]sx?)$/;
2961
+ var SCAN_LIMIT = 2e3;
2962
+ function sourceFiles(tree, dir, out = []) {
2963
+ for (const name of tree.children(dir)) {
2964
+ if (out.length >= SCAN_LIMIT) break;
2965
+ if (name === "node_modules" || name.startsWith(".")) continue;
2966
+ const path = joinPathFragments(dir, name);
2967
+ if (tree.isFile(path)) {
2968
+ if (SOURCE_FILE.test(name)) out.push(path);
2969
+ } else {
2970
+ sourceFiles(tree, path, out);
2971
+ }
2972
+ }
2973
+ return out;
2974
+ }
2975
+ function buildWorkspaceChecks(tree) {
2976
+ const config = readToolkitConfig(tree);
2977
+ if (config?.["frontend"] === "none") return [];
2978
+ const app = resolveFrontendApp(tree, void 0);
2979
+ const recorded = recordedFrontends(config);
2980
+ if (app.kind === "ambiguous" && app.candidates.every((name) => recorded.includes(name))) {
2981
+ return [
2982
+ createDoctorCheck({
2983
+ id: "frontend-resolution",
2984
+ label: "Frontend app",
2985
+ status: "pass",
2986
+ message: `${app.candidates.length} frontend apps, recorded in toolkit.config.json's frontends (pass --project to the slice generators)`,
2987
+ evidence: app.candidates
2988
+ })
2989
+ ];
2990
+ }
2991
+ if (app.kind !== "found") {
2992
+ const resolution = app.kind === "ambiguous" ? createDoctorCheck({
2993
+ id: "frontend-resolution",
2994
+ label: "Frontend app",
2995
+ status: "warning",
2996
+ message: `${app.candidates.length} frontend apps \u2014 the FSD generators and init need to be told which one`,
2997
+ evidence: app.candidates,
2998
+ suggestedAction: `Pass --project=<name> to add-fsd-lite and the slice generators (e.g. --project=${app.candidates[0]}).`
2999
+ }) : app.kind === "none" ? createDoctorCheck({
3000
+ id: "frontend-resolution",
3001
+ label: "Frontend app",
3002
+ status: "warning",
3003
+ message: "no project was recognised as a frontend app",
3004
+ evidence: app.otherProjects,
3005
+ suggestedAction: app.otherProjects.length > 0 ? "If one of the listed projects is the app, pass --project=<name>." : "Create a frontend app (nx g @nx/react:app or @nx/next:app)."
3006
+ }) : createDoctorCheck({
3007
+ id: "frontend-resolution",
3008
+ label: "Frontend app",
3009
+ status: "error",
3010
+ message: app.kind === "invalid-project-json" ? `a project file could not be parsed (${app.detail})` : `--project ${app.project} not found`,
3011
+ evidence: []
3012
+ });
3013
+ return [resolution];
3014
+ }
3015
+ const checks = [
3016
+ createDoctorCheck({
3017
+ id: "frontend-resolution",
3018
+ label: "Frontend app",
3019
+ status: "pass",
3020
+ message: `${app.project}${app.inferredRoot ? " (the workspace root package)" : ""} \u2014 FSD layers live in ${app.sourceRoot}`,
3021
+ evidence: [app.project]
3022
+ }),
3023
+ app.framework === "generic" ? createDoctorCheck({
3024
+ id: "framework-profile",
3025
+ label: "Frontend framework",
3026
+ status: "warning",
3027
+ message: "framework not recognised \u2014 the common FSD rules apply, with no framework-specific layer names",
3028
+ evidence: [app.project],
3029
+ suggestedAction: "If it is React on Vite or Next.js, pass --frontend=react-vite|next (init records it)."
3030
+ }) : createDoctorCheck({ id: "framework-profile", label: "Frontend framework", status: "pass", message: app.framework, evidence: [app.project] })
3031
+ ];
3032
+ const policy = resolveFsdPolicy(config?.["fsd"]);
3033
+ if (policy.sliceGroups !== false) {
3034
+ const lister = (dir) => tree.exists(dir) ? tree.children(dir).map((name) => ({ name, directory: !tree.isFile(joinPathFragments(dir, name)) })) : null;
3035
+ const isGroup = createGroupDetector(app.sourceRoot, policy, (name) => indexFilePattern(policy.platformSuffixes).test(name), lister);
3036
+ const groups = SLICED_LAYER_DIRS.flatMap(
3037
+ (layer) => tree.exists(joinPathFragments(app.sourceRoot, layer)) ? tree.children(joinPathFragments(app.sourceRoot, layer)).filter((name) => isGroup(layer, name)).map((name) => `${layer}/${name}`) : []
3038
+ );
3039
+ checks.push(
3040
+ createDoctorCheck({
3041
+ id: "slice-groups",
3042
+ label: "Slice groups",
3043
+ status: "pass",
3044
+ message: groups.length === 0 ? "no slice groups found" : `the FSD rule treats ${groups.length === 1 ? "this folder" : "these folders"} as groups of separate slices`,
3045
+ evidence: groups
3046
+ })
3047
+ );
3048
+ }
3049
+ const runner = detectUnitTestRunner(tree, app.root);
3050
+ checks.push(
3051
+ runner === "none" ? createDoctorCheck({
3052
+ id: "test-runner",
3053
+ label: "Test runner",
3054
+ status: "warning",
3055
+ message: "no jest or vitest found \u2014 the slice generators write no .spec.ts",
3056
+ evidence: [],
3057
+ suggestedAction: "Add a test runner, or pass --unitTestRunner=jest|vitest to the slice generators."
3058
+ }) : createDoctorCheck({ id: "test-runner", label: "Test runner", status: "pass", message: `${runner} \u2014 the slice generators write a .spec.ts`, evidence: [] })
3059
+ );
3060
+ checks.push(buildPolicySuggestion(tree, app.sourceRoot, policy.sharedPublicApi));
3061
+ return checks;
3062
+ }
3063
+ function buildPolicySuggestion(tree, sourceRoot, current) {
3064
+ const id = "fsd-policy-suggestion";
3065
+ const label = "Shared public API";
3066
+ let atSegment = 0;
3067
+ let deeper = 0;
3068
+ let examples = [];
3069
+ for (const file of sourceFiles(tree, sourceRoot)) {
3070
+ if (/(^|\/)shared\//.test(file.slice(sourceRoot.length))) continue;
3071
+ const text = tree.read(file, "utf-8") ?? "";
3072
+ for (const [, specifier] of text.matchAll(/\bfrom\s+['"]([^'"]+)['"]/g)) {
3073
+ const match = /(?:^|\/|@\/)shared\/(.+)$/.exec(specifier);
3074
+ if (match === null) continue;
3075
+ const parts = match[1].split("/").filter((part) => part !== "" && !/^index(\.[\w.]+)?$/.test(part));
3076
+ if (parts.length <= 1) atSegment++;
3077
+ else {
3078
+ deeper++;
3079
+ if (examples.length < 3) examples = [...examples, specifier];
3080
+ }
3081
+ }
3082
+ }
3083
+ if (current !== "segment" || deeper === 0 || deeper < atSegment) {
3084
+ return createDoctorCheck({ id, label, status: "pass", message: `sharedPublicApi: ${current} fits the imports found`, evidence: [] });
3085
+ }
3086
+ return createDoctorCheck({
3087
+ id,
3088
+ label,
3089
+ status: "warning",
3090
+ message: `${deeper} of ${deeper + atSegment} imports into shared reach past a segment's index \u2014 with sharedPublicApi: segment the FSD rule reports each`,
3091
+ evidence: examples,
3092
+ suggestedAction: 'If that is how this code is meant to be organised, set "fsd": { "sharedPublicApi": "file" } (or "component") in toolkit.config.json.'
3093
+ });
3094
+ }
3095
+
2415
3096
  // src/generators/doctor/generator.ts
2416
3097
  async function doctorGenerator(tree, options) {
2417
- const report = await buildDoctorReportWithIntegrations(tree.root, {
2418
- liveIntegrations: options.liveIntegrations ?? false
3098
+ const base = await buildDoctorReportWithIntegrations(tree.root, {
3099
+ liveIntegrations: options.liveIntegrations ?? false,
3100
+ projects: doctorProjects(tree)
2419
3101
  });
3102
+ const checks = base.overallStatus === "unsupported" ? base.checks : [...base.checks, ...buildWorkspaceChecks(tree)];
3103
+ const report = { checks, overallStatus: base.overallStatus === "unsupported" ? base.overallStatus : computeOverallStatus(checks) };
2420
3104
  if (options.json ?? false) {
2421
3105
  console.log(serializeDoctorReport(report).trimEnd());
2422
3106
  return;