@webpieces/nx-webpieces-rules 0.4.653 → 0.4.654

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/nx-webpieces-rules",
3
- "version": "0.4.653",
3
+ "version": "0.4.654",
4
4
  "description": "Nx-specific webpieces validation rules and graph tooling. Bundles all @webpieces rule packages with Nx graph validators and an inference plugin.",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -18,11 +18,11 @@
18
18
  "README.md"
19
19
  ],
20
20
  "dependencies": {
21
- "@webpieces/ai-hook-rules": "0.4.653",
22
- "@webpieces/code-rules": "0.4.653",
23
- "@webpieces/eslint-rules": "0.4.653",
24
- "@webpieces/pr-gate": "0.4.653",
25
- "@webpieces/rules-config": "0.4.653",
21
+ "@webpieces/ai-hook-rules": "0.4.654",
22
+ "@webpieces/code-rules": "0.4.654",
23
+ "@webpieces/eslint-rules": "0.4.654",
24
+ "@webpieces/pr-gate": "0.4.654",
25
+ "@webpieces/rules-config": "0.4.654",
26
26
  "madge": "8.0.0"
27
27
  },
28
28
  "peerDependencies": {
@@ -12,6 +12,7 @@ exports.default = runExecutor;
12
12
  const rules_config_1 = require("@webpieces/rules-config");
13
13
  const graph_generator_1 = require("../../lib/graph-generator");
14
14
  const graph_sorter_1 = require("../../lib/graph-sorter");
15
+ const graph_cycles_1 = require("../../lib/graph-cycles");
15
16
  const graph_loader_1 = require("../../lib/graph-loader");
16
17
  const graph_metadata_1 = require("../../lib/graph-metadata");
17
18
  const api_scanner_1 = require("../../lib/api-usage/api-scanner");
@@ -122,9 +123,20 @@ async function generateEverything(workspaceRoot, graphPath) {
122
123
  // Step 1: Build the full graph from nx, then transitively reduce it to the view
123
124
  console.log("šŸ“Š Generating dependency graph from nx's project graph...");
124
125
  const reducedGraph = await (0, graph_generator_1.generateReducedGraph)();
126
+ // Step 1b: The graph is a BUILD graph — refuse a cyclic one, naming EVERY cycle. This runs
127
+ // before the sort deliberately: the sort also refuses, but reports one cycle and an
128
+ // undifferentiated list of everything tangled with it, so a repo with several cycles pays one
129
+ // full regeneration per cycle to discover them.
130
+ console.log('šŸ”„ Checking the project graph is acyclic...');
131
+ const cycles = new graph_cycles_1.ProjectCycleDetector();
132
+ cycles.assertAcyclic(reducedGraph, 'the nx project graph');
125
133
  // Step 2: Topological sort (to assign levels for visualization)
126
134
  console.log('šŸ”„ Computing topological layers...');
127
135
  const enhancedGraph = (0, graph_sorter_1.sortGraphTopologically)(reducedGraph);
136
+ // ...and assert the stratification it just produced actually holds: every dependency strictly
137
+ // below its dependent. Safe to assert only because the graph was sorted a line ago — a stale
138
+ // committed file is never checked this way.
139
+ cycles.assertLevelsDescend(reducedGraph, cycles.levelsOf(enhancedGraph), 'the freshly sorted graph');
128
140
  // Step 3: Enrich with AI metadata (framework, shortDescription, file
129
141
  // pointers). This VALIDATES (responsibilities.md required per project)
130
142
  // and throws before any write, so a failure never clobbers the file.
@@ -1 +1 @@
1
- {"version":3,"file":"executor.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/executors/generate/executor.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Generate Executor\n *\n * Generates the architecture dependency graph and saves it to architecture/dependencies.json.\n *\n * Usage:\n * nx run architecture:generate\n */\n\nimport type { ExecutorContext } from '@nx/devkit';\nimport { writeTemplate } from '@webpieces/rules-config';\nimport { generateReducedGraph } from '../../lib/graph-generator';\nimport { sortGraphTopologically } from '../../lib/graph-sorter';\nimport { saveGraph } from '../../lib/graph-loader';\nimport { collectProjectInfo, enrichGraph, MetadataValidationError } from '../../lib/graph-metadata';\nimport { ProjectInfo } from '../../lib/project-info';\nimport {\n scanAndAttachApiRelations,\n describeUnresolvedApiCalls,\n describeNonLiteralDecoratorArgs,\n buildApiContracts,\n describeMismatchedEndpointKinds,\n} from '../../lib/api-usage/api-scanner';\nimport { buildExternalSystems } from '../../lib/api-usage/external-systems';\nimport type { ApiContracts, ExternalSystemDecls } from '../../lib/api-usage/api-relations';\nimport { loadRuntimeConfig } from '../../lib/runtime-config';\nimport type { EnhancedGraph, GraphEntry } from '../../lib/graph-sorter';\nimport { GraphVisualizer } from '../../lib/graph-visualizer';\nimport { deriveRuntimeGraphReport, saveRuntimeGraph } from '../../lib/runtime-graph';\nimport { printAutoHiddenServers } from '../../lib/runtime-participant-resolver';\nimport { toError } from '../../toError';\n\nexport interface GenerateExecutorOptions {\n graphPath?: string;\n}\n\nexport interface ExecutorResult {\n success: boolean;\n}\n\n/**\n * Generate the runtime microservice graph alongside the compile-time graph, DERIVED from the same\n * dependencies.json (its per-project apiRelations) — one regenerate produces both committed files, and\n * validate derives from the SAME source so they can't diverge. rpc APIs become direct runtime edges;\n * pubsub APIs become edges the viz draws through a queue.\n */\n// webpieces-disable no-function-outside-class -- executor step helper, like the rest of this executor file\nfunction generateRuntimeGraph(\n workspaceRoot: string,\n graph: EnhancedGraph,\n hiddenProjects: Set<string>,\n apiContracts: ApiContracts,\n externalSystems: ExternalSystemDecls,\n): void {\n console.log('šŸ“” Deriving runtime graph from dependencies.json (implements Ɨ uses per API)...');\n const report = deriveRuntimeGraphReport(graph, hiddenProjects, apiContracts, externalSystems);\n saveRuntimeGraph(report.graph, workspaceRoot);\n const serviceCount = Object.keys(report.graph.services).length;\n const queueCount = Object.keys(report.graph.queues).length;\n console.log(\n `āœ… Runtime graph saved (${serviceCount} services, ${report.graph.runtimeEdges.length} runtime edges, ` +\n `${queueCount} queues, ${report.graph.triggers.length} cron/external triggers)`,\n );\n // Every edge the derivation had to GUESS at. Loud on purpose: a fanned-out edge is committed and\n // then reasoned about as if it were derived, which is how a fictional call survives for months.\n printRuntimeWarnings(report.warnings);\n printAutoHiddenServers(report.autoHidden);\n // Printed here, FAILED by validate-runtime-architecture: generate must still write the graph,\n // or the validator would have nothing to compare against and the error would be unfixable.\n if (report.problems.length > 0) {\n console.error(`āŒ ${report.problems.length} client call(s) name a service no module answers to:`);\n for (const problem of report.problems) console.error(` • ${problem}`);\n console.error(' This FAILS architecture:validate-runtime-architecture.');\n }\n}\n\n/** Print the derivation's guessed-edge warnings (nothing at all when the graph is fully targeted). */\n// webpieces-disable no-function-outside-class -- executor step helper, like the rest of this executor file\nfunction printRuntimeWarnings(warnings: string[]): void {\n if (warnings.length === 0) return;\n console.warn(`āš ļø ${warnings.length} runtime edge(s) could not be targeted to ONE service:`);\n for (const warning of warnings) console.warn(` • ${warning}`);\n}\n\n/**\n * Scan for api relations and attach them, surfacing any contract we could NOT map back to source.\n * Unresolved contracts mean the graph we are about to WRITE is incomplete, so they must be loud —\n * a green run that quietly omits a service gets committed, rendered, and trusted.\n */\n// webpieces-disable no-function-outside-class -- executor step helper, like the rest of this executor file\nfunction scanApiRelations(\n workspaceRoot: string,\n graph: EnhancedGraph,\n projectInfos: Map<string, ProjectInfo>,\n): ScannedTables {\n console.log('šŸ”Ž Scanning source for implements/uses API relations...');\n const externalApiPaths = loadRuntimeConfig(workspaceRoot).externalApiPaths;\n const scan = scanAndAttachApiRelations(workspaceRoot, graph, projectInfos, externalApiPaths);\n if (scan.unresolvedApiCalls.length > 0) console.warn(describeUnresolvedApiCalls(scan.unresolvedApiCalls));\n // A decorator argument we could not read costs the graph a basePath, a method, or a whole\n // contract — none of which leaves a trace in the output. Name them before anything is written.\n if (scan.nonLiteralDecoratorArgs.length > 0) {\n console.warn(describeNonLiteralDecoratorArgs(scan.nonLiteralDecoratorArgs));\n }\n const contracts = buildApiContracts(scan);\n // An endpoint whose declared trigger its api kind cannot deliver would silently draw a queue or a\n // clock that nothing could ever fire — name it here, where the fix is one decorator away.\n const mismatches = describeMismatchedEndpointKinds(contracts);\n if (mismatches.length > 0) {\n console.error(`āŒ ${mismatches.length} @Endpoint kind(s) conflict with their api kind:`);\n for (const mismatch of mismatches) console.error(` • ${mismatch}`);\n }\n return new ScannedTables(contracts, buildExternalSystems(scan.apiIndex, projectInfos));\n}\n\n/** The two committed tables one scan produces, kept together so callers cannot persist just one. */\nclass ScannedTables {\n constructor(\n public readonly apiContracts: ApiContracts,\n public readonly externalSystems: ExternalSystemDecls,\n ) {}\n}\n\n/** Projects tagged drawOnGraph:false — kept in the JSON, omitted from every rendered graph. */\n// webpieces-disable no-function-outside-class -- executor step helper, like the rest of this executor file\nfunction hiddenProjectsIn(graph: EnhancedGraph): Set<string> {\n const hidden = new Set<string>();\n for (const name of Object.keys(graph)) {\n if (graph[name].drawOnGraph === false) hidden.add(name);\n }\n return hidden;\n}\n\n// webpieces-disable no-function-outside-class -- executor step helper, like the rest of this executor file\nfunction printGraphSummary(graph: EnhancedGraph): void {\n const levels = new Set(Object.values(graph).map((entry: GraphEntry) => entry.level));\n console.log(`\\nšŸ“ˆ Graph Summary:`);\n console.log(` Projects: ${Object.keys(graph).length}`);\n console.log(` Levels: ${levels.size} (0-${Math.max(...levels)})`);\n}\n\n/**\n * The whole generation pipeline. Split out of runExecutor so that function stays a thin\n * try/catch-and-report shell — the error reporting is what a caller reads on failure, and it should\n * not be separated from the throw by fifty lines of steps.\n */\n// webpieces-disable no-function-outside-class -- executor step helper, like the rest of this executor file\nasync function generateEverything(workspaceRoot: string, graphPath: string | undefined): Promise<void> {\n // Step 1: Build the full graph from nx, then transitively reduce it to the view\n console.log(\"šŸ“Š Generating dependency graph from nx's project graph...\");\n const reducedGraph = await generateReducedGraph();\n\n // Step 2: Topological sort (to assign levels for visualization)\n console.log('šŸ”„ Computing topological layers...');\n const enhancedGraph = sortGraphTopologically(reducedGraph);\n\n // Step 3: Enrich with AI metadata (framework, shortDescription, file\n // pointers). This VALIDATES (responsibilities.md required per project)\n // and throws before any write, so a failure never clobbers the file.\n console.log('šŸ·ļø Enriching graph with framework + responsibilities metadata...');\n const projectInfos = await collectProjectInfo();\n enrichGraph(enhancedGraph, projectInfos, workspaceRoot);\n\n // Step 3b: Classify each api-lib edge (implements/uses + rpc/pubsub) by\n // scanning source, so dependencies.json + the viz + the runtime graph all\n // read the same derived truth.\n const scanned = scanApiRelations(workspaceRoot, enhancedGraph, projectInfos);\n const apiContracts = scanned.apiContracts;\n\n // Step 4: Save the graph, INCLUDING the per-contract method table and the external-system\n // declarations the runtime derivation reads back — generate derives from the in-memory graph,\n // validate from the file, so anything not written here would make the two disagree.\n console.log('šŸ’¾ Saving graph to architecture/dependencies.json...');\n saveGraph(enhancedGraph, workspaceRoot, graphPath, apiContracts, scanned.externalSystems);\n console.log('āœ… Graph saved successfully');\n\n // Step 4b: Write the committed, clickable HTML view next to the JSON so\n // dependencies.html regenerates in lock-step with dependencies.json.\n const vizPaths = new GraphVisualizer().writeVisualization(enhancedGraph, workspaceRoot);\n console.log(`āœ… Wrote ${vizPaths.htmlPath}`);\n\n // Step 5: Generate the runtime microservice graph from the same scan.\n // Projects tagged drawOnGraph:false are threaded through so the runtime\n // graph hides them too (they stay flagged in runtime-dependencies.json).\n generateRuntimeGraph(\n workspaceRoot,\n enhancedGraph,\n hiddenProjectsIn(enhancedGraph),\n apiContracts,\n scanned.externalSystems,\n );\n\n printGraphSummary(enhancedGraph);\n}\n\nexport default async function runExecutor(\n options: GenerateExecutorOptions,\n context: ExecutorContext\n): Promise<ExecutorResult> {\n const graphPath = options.graphPath;\n const workspaceRoot = context.root;\n\n console.log('\\nšŸ“Š Architecture Graph Generator\\n');\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n await generateEverything(workspaceRoot, graphPath);\n return { success: true };\n } catch (err: unknown) {\n const error = toError(err);\n console.error('āŒ Graph generation failed:', error.message);\n if (error instanceof MetadataValidationError) {\n const mdPath = writeTemplate(workspaceRoot, 'webpieces.responsibilities.md');\n console.error('');\n console.error('āš ļø *** Refer to ' + mdPath + ' for how to author responsibilities.md files *** āš ļø');\n }\n return { success: false };\n }\n}\n"]}
1
+ {"version":3,"file":"executor.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/executors/generate/executor.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Generate Executor\n *\n * Generates the architecture dependency graph and saves it to architecture/dependencies.json.\n *\n * Usage:\n * nx run architecture:generate\n */\n\nimport type { ExecutorContext } from '@nx/devkit';\nimport { writeTemplate } from '@webpieces/rules-config';\nimport { generateReducedGraph } from '../../lib/graph-generator';\nimport { sortGraphTopologically } from '../../lib/graph-sorter';\nimport { ProjectCycleDetector } from '../../lib/graph-cycles';\nimport { saveGraph } from '../../lib/graph-loader';\nimport { collectProjectInfo, enrichGraph, MetadataValidationError } from '../../lib/graph-metadata';\nimport { ProjectInfo } from '../../lib/project-info';\nimport {\n scanAndAttachApiRelations,\n describeUnresolvedApiCalls,\n describeNonLiteralDecoratorArgs,\n buildApiContracts,\n describeMismatchedEndpointKinds,\n} from '../../lib/api-usage/api-scanner';\nimport { buildExternalSystems } from '../../lib/api-usage/external-systems';\nimport type { ApiContracts, ExternalSystemDecls } from '../../lib/api-usage/api-relations';\nimport { loadRuntimeConfig } from '../../lib/runtime-config';\nimport type { EnhancedGraph, GraphEntry } from '../../lib/graph-sorter';\nimport { GraphVisualizer } from '../../lib/graph-visualizer';\nimport { deriveRuntimeGraphReport, saveRuntimeGraph } from '../../lib/runtime-graph';\nimport { printAutoHiddenServers } from '../../lib/runtime-participant-resolver';\nimport { toError } from '../../toError';\n\nexport interface GenerateExecutorOptions {\n graphPath?: string;\n}\n\nexport interface ExecutorResult {\n success: boolean;\n}\n\n/**\n * Generate the runtime microservice graph alongside the compile-time graph, DERIVED from the same\n * dependencies.json (its per-project apiRelations) — one regenerate produces both committed files, and\n * validate derives from the SAME source so they can't diverge. rpc APIs become direct runtime edges;\n * pubsub APIs become edges the viz draws through a queue.\n */\n// webpieces-disable no-function-outside-class -- executor step helper, like the rest of this executor file\nfunction generateRuntimeGraph(\n workspaceRoot: string,\n graph: EnhancedGraph,\n hiddenProjects: Set<string>,\n apiContracts: ApiContracts,\n externalSystems: ExternalSystemDecls,\n): void {\n console.log('šŸ“” Deriving runtime graph from dependencies.json (implements Ɨ uses per API)...');\n const report = deriveRuntimeGraphReport(graph, hiddenProjects, apiContracts, externalSystems);\n saveRuntimeGraph(report.graph, workspaceRoot);\n const serviceCount = Object.keys(report.graph.services).length;\n const queueCount = Object.keys(report.graph.queues).length;\n console.log(\n `āœ… Runtime graph saved (${serviceCount} services, ${report.graph.runtimeEdges.length} runtime edges, ` +\n `${queueCount} queues, ${report.graph.triggers.length} cron/external triggers)`,\n );\n // Every edge the derivation had to GUESS at. Loud on purpose: a fanned-out edge is committed and\n // then reasoned about as if it were derived, which is how a fictional call survives for months.\n printRuntimeWarnings(report.warnings);\n printAutoHiddenServers(report.autoHidden);\n // Printed here, FAILED by validate-runtime-architecture: generate must still write the graph,\n // or the validator would have nothing to compare against and the error would be unfixable.\n if (report.problems.length > 0) {\n console.error(`āŒ ${report.problems.length} client call(s) name a service no module answers to:`);\n for (const problem of report.problems) console.error(` • ${problem}`);\n console.error(' This FAILS architecture:validate-runtime-architecture.');\n }\n}\n\n/** Print the derivation's guessed-edge warnings (nothing at all when the graph is fully targeted). */\n// webpieces-disable no-function-outside-class -- executor step helper, like the rest of this executor file\nfunction printRuntimeWarnings(warnings: string[]): void {\n if (warnings.length === 0) return;\n console.warn(`āš ļø ${warnings.length} runtime edge(s) could not be targeted to ONE service:`);\n for (const warning of warnings) console.warn(` • ${warning}`);\n}\n\n/**\n * Scan for api relations and attach them, surfacing any contract we could NOT map back to source.\n * Unresolved contracts mean the graph we are about to WRITE is incomplete, so they must be loud —\n * a green run that quietly omits a service gets committed, rendered, and trusted.\n */\n// webpieces-disable no-function-outside-class -- executor step helper, like the rest of this executor file\nfunction scanApiRelations(\n workspaceRoot: string,\n graph: EnhancedGraph,\n projectInfos: Map<string, ProjectInfo>,\n): ScannedTables {\n console.log('šŸ”Ž Scanning source for implements/uses API relations...');\n const externalApiPaths = loadRuntimeConfig(workspaceRoot).externalApiPaths;\n const scan = scanAndAttachApiRelations(workspaceRoot, graph, projectInfos, externalApiPaths);\n if (scan.unresolvedApiCalls.length > 0) console.warn(describeUnresolvedApiCalls(scan.unresolvedApiCalls));\n // A decorator argument we could not read costs the graph a basePath, a method, or a whole\n // contract — none of which leaves a trace in the output. Name them before anything is written.\n if (scan.nonLiteralDecoratorArgs.length > 0) {\n console.warn(describeNonLiteralDecoratorArgs(scan.nonLiteralDecoratorArgs));\n }\n const contracts = buildApiContracts(scan);\n // An endpoint whose declared trigger its api kind cannot deliver would silently draw a queue or a\n // clock that nothing could ever fire — name it here, where the fix is one decorator away.\n const mismatches = describeMismatchedEndpointKinds(contracts);\n if (mismatches.length > 0) {\n console.error(`āŒ ${mismatches.length} @Endpoint kind(s) conflict with their api kind:`);\n for (const mismatch of mismatches) console.error(` • ${mismatch}`);\n }\n return new ScannedTables(contracts, buildExternalSystems(scan.apiIndex, projectInfos));\n}\n\n/** The two committed tables one scan produces, kept together so callers cannot persist just one. */\nclass ScannedTables {\n constructor(\n public readonly apiContracts: ApiContracts,\n public readonly externalSystems: ExternalSystemDecls,\n ) {}\n}\n\n/** Projects tagged drawOnGraph:false — kept in the JSON, omitted from every rendered graph. */\n// webpieces-disable no-function-outside-class -- executor step helper, like the rest of this executor file\nfunction hiddenProjectsIn(graph: EnhancedGraph): Set<string> {\n const hidden = new Set<string>();\n for (const name of Object.keys(graph)) {\n if (graph[name].drawOnGraph === false) hidden.add(name);\n }\n return hidden;\n}\n\n// webpieces-disable no-function-outside-class -- executor step helper, like the rest of this executor file\nfunction printGraphSummary(graph: EnhancedGraph): void {\n const levels = new Set(Object.values(graph).map((entry: GraphEntry) => entry.level));\n console.log(`\\nšŸ“ˆ Graph Summary:`);\n console.log(` Projects: ${Object.keys(graph).length}`);\n console.log(` Levels: ${levels.size} (0-${Math.max(...levels)})`);\n}\n\n/**\n * The whole generation pipeline. Split out of runExecutor so that function stays a thin\n * try/catch-and-report shell — the error reporting is what a caller reads on failure, and it should\n * not be separated from the throw by fifty lines of steps.\n */\n// webpieces-disable no-function-outside-class -- executor step helper, like the rest of this executor file\nasync function generateEverything(workspaceRoot: string, graphPath: string | undefined): Promise<void> {\n // Step 1: Build the full graph from nx, then transitively reduce it to the view\n console.log(\"šŸ“Š Generating dependency graph from nx's project graph...\");\n const reducedGraph = await generateReducedGraph();\n\n // Step 1b: The graph is a BUILD graph — refuse a cyclic one, naming EVERY cycle. This runs\n // before the sort deliberately: the sort also refuses, but reports one cycle and an\n // undifferentiated list of everything tangled with it, so a repo with several cycles pays one\n // full regeneration per cycle to discover them.\n console.log('šŸ”„ Checking the project graph is acyclic...');\n const cycles = new ProjectCycleDetector();\n cycles.assertAcyclic(reducedGraph, 'the nx project graph');\n\n // Step 2: Topological sort (to assign levels for visualization)\n console.log('šŸ”„ Computing topological layers...');\n const enhancedGraph = sortGraphTopologically(reducedGraph);\n // ...and assert the stratification it just produced actually holds: every dependency strictly\n // below its dependent. Safe to assert only because the graph was sorted a line ago — a stale\n // committed file is never checked this way.\n cycles.assertLevelsDescend(reducedGraph, cycles.levelsOf(enhancedGraph), 'the freshly sorted graph');\n\n // Step 3: Enrich with AI metadata (framework, shortDescription, file\n // pointers). This VALIDATES (responsibilities.md required per project)\n // and throws before any write, so a failure never clobbers the file.\n console.log('šŸ·ļø Enriching graph with framework + responsibilities metadata...');\n const projectInfos = await collectProjectInfo();\n enrichGraph(enhancedGraph, projectInfos, workspaceRoot);\n\n // Step 3b: Classify each api-lib edge (implements/uses + rpc/pubsub) by\n // scanning source, so dependencies.json + the viz + the runtime graph all\n // read the same derived truth.\n const scanned = scanApiRelations(workspaceRoot, enhancedGraph, projectInfos);\n const apiContracts = scanned.apiContracts;\n\n // Step 4: Save the graph, INCLUDING the per-contract method table and the external-system\n // declarations the runtime derivation reads back — generate derives from the in-memory graph,\n // validate from the file, so anything not written here would make the two disagree.\n console.log('šŸ’¾ Saving graph to architecture/dependencies.json...');\n saveGraph(enhancedGraph, workspaceRoot, graphPath, apiContracts, scanned.externalSystems);\n console.log('āœ… Graph saved successfully');\n\n // Step 4b: Write the committed, clickable HTML view next to the JSON so\n // dependencies.html regenerates in lock-step with dependencies.json.\n const vizPaths = new GraphVisualizer().writeVisualization(enhancedGraph, workspaceRoot);\n console.log(`āœ… Wrote ${vizPaths.htmlPath}`);\n\n // Step 5: Generate the runtime microservice graph from the same scan.\n // Projects tagged drawOnGraph:false are threaded through so the runtime\n // graph hides them too (they stay flagged in runtime-dependencies.json).\n generateRuntimeGraph(\n workspaceRoot,\n enhancedGraph,\n hiddenProjectsIn(enhancedGraph),\n apiContracts,\n scanned.externalSystems,\n );\n\n printGraphSummary(enhancedGraph);\n}\n\nexport default async function runExecutor(\n options: GenerateExecutorOptions,\n context: ExecutorContext\n): Promise<ExecutorResult> {\n const graphPath = options.graphPath;\n const workspaceRoot = context.root;\n\n console.log('\\nšŸ“Š Architecture Graph Generator\\n');\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n await generateEverything(workspaceRoot, graphPath);\n return { success: true };\n } catch (err: unknown) {\n const error = toError(err);\n console.error('āŒ Graph generation failed:', error.message);\n if (error instanceof MetadataValidationError) {\n const mdPath = writeTemplate(workspaceRoot, 'webpieces.responsibilities.md');\n console.error('');\n console.error('āš ļø *** Refer to ' + mdPath + ' for how to author responsibilities.md files *** āš ļø');\n }\n return { success: false };\n }\n}\n"]}
@@ -12,6 +12,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.default = runExecutor;
13
13
  const graph_generator_1 = require("../../lib/graph-generator");
14
14
  const graph_sorter_1 = require("../../lib/graph-sorter");
15
+ const graph_cycles_1 = require("../../lib/graph-cycles");
15
16
  const rule_gate_1 = require("../../lib/rule-gate");
16
17
  const toError_1 = require("../../toError");
17
18
  async function runExecutor(_options, context) {
@@ -26,11 +27,19 @@ async function runExecutor(_options, context) {
26
27
  // Step 1: Generate current graph from project.json files
27
28
  console.log('šŸ“Š Generating dependency graph from project.json files...');
28
29
  const rawGraph = await (0, graph_generator_1.generateGraph)();
29
- // Step 2: Topological sort (validates acyclic)
30
- console.log('šŸ”„ Checking for cycles (topological sort)...');
30
+ // Step 2: Enumerate EVERY cycle (Tarjan), then stratify.
31
+ //
32
+ // The detector runs first on purpose: the topological sort also refuses a cyclic graph, but
33
+ // reports one cycle and an undifferentiated "among: ..." list, so a repo with several cycles
34
+ // pays one full run per cycle to discover them all. It walks PROJECT KEYS, never display
35
+ // names — fusing `@scope/x` with `x` would invent a cycle that does not exist.
36
+ console.log('šŸ”„ Checking for cycles (all strongly connected components)...');
31
37
  // eslint-disable-next-line @webpieces/no-unmanaged-exceptions
32
38
  try {
33
- (0, graph_sorter_1.sortGraphTopologically)(rawGraph);
39
+ const detector = new graph_cycles_1.ProjectCycleDetector();
40
+ detector.assertAcyclic(rawGraph, 'the nx project graph');
41
+ const graph = (0, graph_sorter_1.sortGraphTopologically)(rawGraph);
42
+ detector.assertLevelsDescend(rawGraph, detector.levelsOf(graph), 'the freshly sorted graph');
34
43
  console.log('āœ… No circular dependencies detected!');
35
44
  // Print summary
36
45
  const projectCount = Object.keys(rawGraph).length;
@@ -41,10 +50,6 @@ async function runExecutor(_options, context) {
41
50
  const error = (0, toError_1.toError)(err);
42
51
  console.error('āŒ Circular dependency detected!');
43
52
  console.error(error.message);
44
- console.error('\nTo fix:');
45
- console.error(' 1. Review the cycle above');
46
- console.error(' 2. Break the cycle by refactoring dependencies');
47
- console.error(' 3. Run this check again');
48
53
  return { success: false };
49
54
  }
50
55
  }
@@ -1 +1 @@
1
- {"version":3,"file":"executor.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/executors/validate-no-architecture-cycles/executor.ts"],"names":[],"mappings":";AAAA;;;;;;;;GAQG;;AAgBH,8BA6CC;AA1DD,+DAA0D;AAC1D,yDAAgE;AAChE,mDAA+C;AAC/C,2CAAwC;AAUzB,KAAK,UAAU,WAAW,CACrC,QAAiC,EACjC,OAAwB;IAExB,8FAA8F;IAC9F,yFAAyF;IACzF,IAAI,IAAI,oBAAQ,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,EAAE,iCAAiC,EAAE,IAAI,CAAC,EAAE,CAAC;QACnF,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IAC7B,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,4CAA4C,CAAC,CAAC;IAE1D,8DAA8D;IAC9D,IAAI,CAAC;QACD,yDAAyD;QACzD,OAAO,CAAC,GAAG,CAAC,2DAA2D,CAAC,CAAC;QACzE,MAAM,QAAQ,GAAG,MAAM,IAAA,+BAAa,GAAE,CAAC;QAEvC,+CAA+C;QAC/C,OAAO,CAAC,GAAG,CAAC,8CAA8C,CAAC,CAAC;QAC5D,8DAA8D;QAC9D,IAAI,CAAC;YACD,IAAA,qCAAsB,EAAC,QAAQ,CAAC,CAAC;YACjC,OAAO,CAAC,GAAG,CAAC,sCAAsC,CAAC,CAAC;YAEpD,gBAAgB;YAChB,MAAM,YAAY,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC;YAClD,OAAO,CAAC,GAAG,CAAC,iBAAiB,YAAY,wBAAwB,CAAC,CAAC;YAEnE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;QAC7B,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACpB,MAAM,KAAK,GAAG,IAAA,iBAAO,EAAC,GAAG,CAAC,CAAC;YAC3B,OAAO,CAAC,KAAK,CAAC,iCAAiC,CAAC,CAAC;YACjD,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;YAC7B,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;YAC3B,OAAO,CAAC,KAAK,CAAC,6BAA6B,CAAC,CAAC;YAC7C,OAAO,CAAC,KAAK,CAAC,kDAAkD,CAAC,CAAC;YAClE,OAAO,CAAC,KAAK,CAAC,2BAA2B,CAAC,CAAC;YAC3C,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;QAC9B,CAAC;IACL,CAAC;IAAC,OAAO,GAAY,EAAE,CAAC;QACpB,MAAM,KAAK,GAAG,IAAA,iBAAO,EAAC,GAAG,CAAC,CAAC;QAC3B,OAAO,CAAC,KAAK,CAAC,4BAA4B,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;QAC3D,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IAC9B,CAAC;AACL,CAAC","sourcesContent":["/**\n * Validate No Architecture Cycles Executor\n *\n * Validates that the architecture dependency graph has no circular dependencies.\n * This is a fast check that only validates acyclicity at the project level.\n *\n * Usage:\n * nx run architecture:validate-no-architecture-cycles\n */\n\nimport type { ExecutorContext } from '@nx/devkit';\nimport { generateGraph } from '../../lib/graph-generator';\nimport { sortGraphTopologically } from '../../lib/graph-sorter';\nimport { RuleGate } from '../../lib/rule-gate';\nimport { toError } from '../../toError';\n\nexport interface ValidateNoCyclesOptions {\n // No options needed\n}\n\nexport interface ExecutorResult {\n success: boolean;\n}\n\nexport default async function runExecutor(\n _options: ValidateNoCyclesOptions,\n context: ExecutorContext\n): Promise<ExecutorResult> {\n // Epoch-gateable: the existing cycle set can be grandfathered while a refactor lands, so this\n // rule honors turnOffRuleUntilEpoch / turnOffRuleWhileOnBranch in addition to mode: OFF.\n if (new RuleGate().isDisabled(context.root, 'validate-no-architecture-cycles', true)) {\n return { success: true };\n }\n\n console.log('\\nšŸ”„ Validating No Circular Dependencies\\n');\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // Step 1: Generate current graph from project.json files\n console.log('šŸ“Š Generating dependency graph from project.json files...');\n const rawGraph = await generateGraph();\n\n // Step 2: Topological sort (validates acyclic)\n console.log('šŸ”„ Checking for cycles (topological sort)...');\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n sortGraphTopologically(rawGraph);\n console.log('āœ… No circular dependencies detected!');\n\n // Print summary\n const projectCount = Object.keys(rawGraph).length;\n console.log(`\\nšŸ“ˆ Summary: ${projectCount} projects, all acyclic`);\n\n return { success: true };\n } catch (err: unknown) {\n const error = toError(err);\n console.error('āŒ Circular dependency detected!');\n console.error(error.message);\n console.error('\\nTo fix:');\n console.error(' 1. Review the cycle above');\n console.error(' 2. Break the cycle by refactoring dependencies');\n console.error(' 3. Run this check again');\n return { success: false };\n }\n } catch (err: unknown) {\n const error = toError(err);\n console.error('āŒ Cycle validation failed:', error.message);\n return { success: false };\n }\n}\n"]}
1
+ {"version":3,"file":"executor.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/executors/validate-no-architecture-cycles/executor.ts"],"names":[],"mappings":";AAAA;;;;;;;;GAQG;;AAiBH,8BAiDC;AA/DD,+DAA0D;AAC1D,yDAAgE;AAChE,yDAA8D;AAC9D,mDAA+C;AAC/C,2CAAwC;AAUzB,KAAK,UAAU,WAAW,CACrC,QAAiC,EACjC,OAAwB;IAExB,8FAA8F;IAC9F,yFAAyF;IACzF,IAAI,IAAI,oBAAQ,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,EAAE,iCAAiC,EAAE,IAAI,CAAC,EAAE,CAAC;QACnF,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IAC7B,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,4CAA4C,CAAC,CAAC;IAE1D,8DAA8D;IAC9D,IAAI,CAAC;QACD,yDAAyD;QACzD,OAAO,CAAC,GAAG,CAAC,2DAA2D,CAAC,CAAC;QACzE,MAAM,QAAQ,GAAG,MAAM,IAAA,+BAAa,GAAE,CAAC;QAEvC,yDAAyD;QACzD,EAAE;QACF,4FAA4F;QAC5F,6FAA6F;QAC7F,yFAAyF;QACzF,+EAA+E;QAC/E,OAAO,CAAC,GAAG,CAAC,+DAA+D,CAAC,CAAC;QAC7E,8DAA8D;QAC9D,IAAI,CAAC;YACD,MAAM,QAAQ,GAAG,IAAI,mCAAoB,EAAE,CAAC;YAC5C,QAAQ,CAAC,aAAa,CAAC,QAAQ,EAAE,sBAAsB,CAAC,CAAC;YACzD,MAAM,KAAK,GAAG,IAAA,qCAAsB,EAAC,QAAQ,CAAC,CAAC;YAC/C,QAAQ,CAAC,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,0BAA0B,CAAC,CAAC;YAC7F,OAAO,CAAC,GAAG,CAAC,sCAAsC,CAAC,CAAC;YAEpD,gBAAgB;YAChB,MAAM,YAAY,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC;YAClD,OAAO,CAAC,GAAG,CAAC,iBAAiB,YAAY,wBAAwB,CAAC,CAAC;YAEnE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;QAC7B,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACpB,MAAM,KAAK,GAAG,IAAA,iBAAO,EAAC,GAAG,CAAC,CAAC;YAC3B,OAAO,CAAC,KAAK,CAAC,iCAAiC,CAAC,CAAC;YACjD,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;YAC7B,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;QAC9B,CAAC;IACL,CAAC;IAAC,OAAO,GAAY,EAAE,CAAC;QACpB,MAAM,KAAK,GAAG,IAAA,iBAAO,EAAC,GAAG,CAAC,CAAC;QAC3B,OAAO,CAAC,KAAK,CAAC,4BAA4B,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;QAC3D,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IAC9B,CAAC;AACL,CAAC","sourcesContent":["/**\n * Validate No Architecture Cycles Executor\n *\n * Validates that the architecture dependency graph has no circular dependencies.\n * This is a fast check that only validates acyclicity at the project level.\n *\n * Usage:\n * nx run architecture:validate-no-architecture-cycles\n */\n\nimport type { ExecutorContext } from '@nx/devkit';\nimport { generateGraph } from '../../lib/graph-generator';\nimport { sortGraphTopologically } from '../../lib/graph-sorter';\nimport { ProjectCycleDetector } from '../../lib/graph-cycles';\nimport { RuleGate } from '../../lib/rule-gate';\nimport { toError } from '../../toError';\n\nexport interface ValidateNoCyclesOptions {\n // No options needed\n}\n\nexport interface ExecutorResult {\n success: boolean;\n}\n\nexport default async function runExecutor(\n _options: ValidateNoCyclesOptions,\n context: ExecutorContext\n): Promise<ExecutorResult> {\n // Epoch-gateable: the existing cycle set can be grandfathered while a refactor lands, so this\n // rule honors turnOffRuleUntilEpoch / turnOffRuleWhileOnBranch in addition to mode: OFF.\n if (new RuleGate().isDisabled(context.root, 'validate-no-architecture-cycles', true)) {\n return { success: true };\n }\n\n console.log('\\nšŸ”„ Validating No Circular Dependencies\\n');\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // Step 1: Generate current graph from project.json files\n console.log('šŸ“Š Generating dependency graph from project.json files...');\n const rawGraph = await generateGraph();\n\n // Step 2: Enumerate EVERY cycle (Tarjan), then stratify.\n //\n // The detector runs first on purpose: the topological sort also refuses a cyclic graph, but\n // reports one cycle and an undifferentiated \"among: ...\" list, so a repo with several cycles\n // pays one full run per cycle to discover them all. It walks PROJECT KEYS, never display\n // names — fusing `@scope/x` with `x` would invent a cycle that does not exist.\n console.log('šŸ”„ Checking for cycles (all strongly connected components)...');\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const detector = new ProjectCycleDetector();\n detector.assertAcyclic(rawGraph, 'the nx project graph');\n const graph = sortGraphTopologically(rawGraph);\n detector.assertLevelsDescend(rawGraph, detector.levelsOf(graph), 'the freshly sorted graph');\n console.log('āœ… No circular dependencies detected!');\n\n // Print summary\n const projectCount = Object.keys(rawGraph).length;\n console.log(`\\nšŸ“ˆ Summary: ${projectCount} projects, all acyclic`);\n\n return { success: true };\n } catch (err: unknown) {\n const error = toError(err);\n console.error('āŒ Circular dependency detected!');\n console.error(error.message);\n return { success: false };\n }\n } catch (err: unknown) {\n const error = toError(err);\n console.error('āŒ Cycle validation failed:', error.message);\n return { success: false };\n }\n}\n"]}
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Project cycle detection for the compile-time dependency graph.
3
+ *
4
+ * The architecture graph is a BUILD graph: a cycle in it is not a style problem, it is a graph that
5
+ * cannot be built in any order, and the level numbers every rendered row is keyed on are meaningless
6
+ * the moment one exists.
7
+ *
8
+ * `sortGraphTopologically` already refuses to stratify a cyclic graph, but it reports exactly ONE
9
+ * cycle plus an undifferentiated "among: a, b, c, d, e" list — so a repo with three independent
10
+ * cycles fixes one, re-runs, finds another, three times. This enumerates ALL of them up front, each
11
+ * as a concrete `a -> b -> c -> a` chain of PROJECT KEYS, so one run names everything to break.
12
+ *
13
+ * IT OPERATES ON PROJECT KEYS, NEVER ON DISPLAY NAMES. A workspace may hold both
14
+ * `@scope/public-api` and `public-api`; running detection on scope-stripped names would fuse them
15
+ * and report their perfectly legal L6 → L0 edge as a self-loop cycle. See graph-names.ts.
16
+ *
17
+ * Self-loops count: a project listing itself in `dependsOn` is the degenerate cycle and fails too.
18
+ */
19
+ /** A directed dependency graph keyed on PROJECT KEYS: `graph[project]` is what `project` depends on. */
20
+ export type ProjectAdjacency = Record<string, string[]>;
21
+ /**
22
+ * The one field the level assertion reads off a project entry.
23
+ *
24
+ * Spelled here rather than importing `EnhancedGraph` from graph-sorter on purpose: graph-sorter
25
+ * imports THIS module (it reports its Kahn stall through the detector), and even a type-only import
26
+ * back closes a file-import cycle the build refuses. `EnhancedGraph` satisfies this structurally.
27
+ */
28
+ export declare class ProjectLevel {
29
+ readonly level: number;
30
+ constructor(level: number);
31
+ }
32
+ /** Any project map carrying levels — what {@link ProjectCycleDetector.levelsOf} reads. */
33
+ export type LevelledProjects = Record<string, ProjectLevel>;
34
+ /** One cycle, as the ordered chain of project keys that closes it (first key repeated at the end). */
35
+ export declare class ProjectCycle {
36
+ readonly path: string[];
37
+ constructor(path: string[]);
38
+ /** `a -> b -> c -> a`, the form a reader can act on directly. */
39
+ describe(): string;
40
+ }
41
+ /** One edge whose direction contradicts the levels assigned to its endpoints. */
42
+ export declare class LevelViolation {
43
+ readonly from: string;
44
+ readonly fromLevel: number;
45
+ readonly to: string;
46
+ readonly toLevel: number;
47
+ constructor(from: string, fromLevel: number, to: string, toLevel: number);
48
+ describe(): string;
49
+ }
50
+ /** Thrown when the compile-time project graph is not a DAG. */
51
+ export declare class CircularProjectDependencyError extends Error {
52
+ constructor(message: string);
53
+ }
54
+ /** Thrown when a freshly stratified graph contains an edge that its own levels forbid. */
55
+ export declare class LevelViolationError extends Error {
56
+ constructor(message: string);
57
+ }
58
+ export declare class ProjectCycleDetector {
59
+ /**
60
+ * Every cycle in the graph, deterministically ordered.
61
+ *
62
+ * Tarjan (shared with the runtime graph's detector — one SCC implementation, two callers) gives
63
+ * the strongly connected components; each component is then walked to recover an actual ordered
64
+ * path, because "these five projects are tangled" is not something a reader can act on and
65
+ * `a -> b -> c -> a` is.
66
+ */
67
+ find(graph: ProjectAdjacency): ProjectCycle[];
68
+ /**
69
+ * Fail the generation on any cycle, naming ALL of them.
70
+ *
71
+ * @param source what the graph was read from, so the message says WHERE to go fix it.
72
+ */
73
+ assertAcyclic(graph: ProjectAdjacency, source: string): void;
74
+ /**
75
+ * The level each project was stratified onto — the input {@link assertLevelsDescend} checks
76
+ * against. Only `level` is read off each entry.
77
+ */
78
+ levelsOf(graph: LevelledProjects): Map<string, number>;
79
+ /**
80
+ * Edges that contradict the levels assigned to their endpoints.
81
+ *
82
+ * `sortGraphTopologically` stratifies so that every dependency sits STRICTLY below its
83
+ * dependent, so on a freshly sorted graph this can only fire if the stratification itself broke
84
+ * — it is an assertion about the sorter, not a rule about repos. Callers must therefore only run
85
+ * it against a graph they just sorted, never against a possibly-stale committed file.
86
+ */
87
+ findLevelViolations(graph: ProjectAdjacency, levelOf: Map<string, number>): LevelViolation[];
88
+ /** Fail loudly when a just-sorted graph disagrees with its own levels. */
89
+ assertLevelsDescend(graph: ProjectAdjacency, levelOf: Map<string, number>, source: string): void;
90
+ /**
91
+ * Recover one ordered cycle from a strongly connected component: walk forward inside the
92
+ * component until a node repeats, then return the closed chain from that repeat onward. A
93
+ * one-node component only reaches here when it carries a self-edge, which yields `x -> x`.
94
+ */
95
+ private pathWithin;
96
+ }
@@ -0,0 +1,182 @@
1
+ "use strict";
2
+ /**
3
+ * Project cycle detection for the compile-time dependency graph.
4
+ *
5
+ * The architecture graph is a BUILD graph: a cycle in it is not a style problem, it is a graph that
6
+ * cannot be built in any order, and the level numbers every rendered row is keyed on are meaningless
7
+ * the moment one exists.
8
+ *
9
+ * `sortGraphTopologically` already refuses to stratify a cyclic graph, but it reports exactly ONE
10
+ * cycle plus an undifferentiated "among: a, b, c, d, e" list — so a repo with three independent
11
+ * cycles fixes one, re-runs, finds another, three times. This enumerates ALL of them up front, each
12
+ * as a concrete `a -> b -> c -> a` chain of PROJECT KEYS, so one run names everything to break.
13
+ *
14
+ * IT OPERATES ON PROJECT KEYS, NEVER ON DISPLAY NAMES. A workspace may hold both
15
+ * `@scope/public-api` and `public-api`; running detection on scope-stripped names would fuse them
16
+ * and report their perfectly legal L6 → L0 edge as a self-loop cycle. See graph-names.ts.
17
+ *
18
+ * Self-loops count: a project listing itself in `dependsOn` is the degenerate cycle and fails too.
19
+ */
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.ProjectCycleDetector = exports.LevelViolationError = exports.CircularProjectDependencyError = exports.LevelViolation = exports.ProjectCycle = exports.ProjectLevel = void 0;
22
+ const runtime_cycles_1 = require("./runtime-cycles");
23
+ /**
24
+ * The one field the level assertion reads off a project entry.
25
+ *
26
+ * Spelled here rather than importing `EnhancedGraph` from graph-sorter on purpose: graph-sorter
27
+ * imports THIS module (it reports its Kahn stall through the detector), and even a type-only import
28
+ * back closes a file-import cycle the build refuses. `EnhancedGraph` satisfies this structurally.
29
+ */
30
+ class ProjectLevel {
31
+ level;
32
+ constructor(level) {
33
+ this.level = level;
34
+ }
35
+ }
36
+ exports.ProjectLevel = ProjectLevel;
37
+ /** One cycle, as the ordered chain of project keys that closes it (first key repeated at the end). */
38
+ class ProjectCycle {
39
+ path;
40
+ constructor(path) {
41
+ this.path = path;
42
+ }
43
+ /** `a -> b -> c -> a`, the form a reader can act on directly. */
44
+ describe() {
45
+ return this.path.join(' -> ');
46
+ }
47
+ }
48
+ exports.ProjectCycle = ProjectCycle;
49
+ /** One edge whose direction contradicts the levels assigned to its endpoints. */
50
+ class LevelViolation {
51
+ from;
52
+ fromLevel;
53
+ to;
54
+ toLevel;
55
+ constructor(from, fromLevel, to, toLevel) {
56
+ this.from = from;
57
+ this.fromLevel = fromLevel;
58
+ this.to = to;
59
+ this.toLevel = toLevel;
60
+ }
61
+ describe() {
62
+ return `${this.from} (L${this.fromLevel}) -> ${this.to} (L${this.toLevel})`;
63
+ }
64
+ }
65
+ exports.LevelViolation = LevelViolation;
66
+ /** Thrown when the compile-time project graph is not a DAG. */
67
+ class CircularProjectDependencyError extends Error {
68
+ constructor(message) {
69
+ super(message);
70
+ this.name = 'CircularProjectDependencyError';
71
+ }
72
+ }
73
+ exports.CircularProjectDependencyError = CircularProjectDependencyError;
74
+ /** Thrown when a freshly stratified graph contains an edge that its own levels forbid. */
75
+ class LevelViolationError extends Error {
76
+ constructor(message) {
77
+ super(message);
78
+ this.name = 'LevelViolationError';
79
+ }
80
+ }
81
+ exports.LevelViolationError = LevelViolationError;
82
+ class ProjectCycleDetector {
83
+ /**
84
+ * Every cycle in the graph, deterministically ordered.
85
+ *
86
+ * Tarjan (shared with the runtime graph's detector — one SCC implementation, two callers) gives
87
+ * the strongly connected components; each component is then walked to recover an actual ordered
88
+ * path, because "these five projects are tangled" is not something a reader can act on and
89
+ * `a -> b -> c -> a` is.
90
+ */
91
+ find(graph) {
92
+ const cycles = (0, runtime_cycles_1.findRuntimeCycles)(graph).map((scc) => new ProjectCycle(this.pathWithin(graph, new Set(scc.services))));
93
+ return cycles.sort((a, b) => a.describe().localeCompare(b.describe()));
94
+ }
95
+ /**
96
+ * Fail the generation on any cycle, naming ALL of them.
97
+ *
98
+ * @param source what the graph was read from, so the message says WHERE to go fix it.
99
+ */
100
+ assertAcyclic(graph, source) {
101
+ const cycles = this.find(graph);
102
+ if (cycles.length === 0)
103
+ return;
104
+ const listed = cycles.map((cycle) => ` ${cycle.describe()}`).join('\n');
105
+ const plural = cycles.length === 1 ? 'cycle' : 'cycles';
106
+ throw new CircularProjectDependencyError(`${source}: the project dependency graph is a BUILD graph and must be acyclic, but it `
107
+ + `contains ${cycles.length} ${plural}:\n${listed}\n`
108
+ + 'Fix: break each chain above by removing one edge in it — move the shared code into a '
109
+ + 'lower-level library both sides depend on, or invert the dependency behind an '
110
+ + 'api-lib contract. Every project on a chain is unbuildable until it is broken.');
111
+ }
112
+ /**
113
+ * The level each project was stratified onto — the input {@link assertLevelsDescend} checks
114
+ * against. Only `level` is read off each entry.
115
+ */
116
+ levelsOf(graph) {
117
+ const levels = new Map();
118
+ for (const project of Object.keys(graph))
119
+ levels.set(project, graph[project].level);
120
+ return levels;
121
+ }
122
+ /**
123
+ * Edges that contradict the levels assigned to their endpoints.
124
+ *
125
+ * `sortGraphTopologically` stratifies so that every dependency sits STRICTLY below its
126
+ * dependent, so on a freshly sorted graph this can only fire if the stratification itself broke
127
+ * — it is an assertion about the sorter, not a rule about repos. Callers must therefore only run
128
+ * it against a graph they just sorted, never against a possibly-stale committed file.
129
+ */
130
+ findLevelViolations(graph, levelOf) {
131
+ const violations = [];
132
+ for (const project of Object.keys(graph)) {
133
+ const fromLevel = levelOf.get(project);
134
+ if (fromLevel === undefined)
135
+ continue;
136
+ for (const dep of graph[project] ?? []) {
137
+ const toLevel = levelOf.get(dep);
138
+ if (toLevel === undefined || fromLevel > toLevel)
139
+ continue;
140
+ violations.push(new LevelViolation(project, fromLevel, dep, toLevel));
141
+ }
142
+ }
143
+ return violations;
144
+ }
145
+ /** Fail loudly when a just-sorted graph disagrees with its own levels. */
146
+ assertLevelsDescend(graph, levelOf, source) {
147
+ const violations = this.findLevelViolations(graph, levelOf);
148
+ if (violations.length === 0)
149
+ return;
150
+ const listed = violations
151
+ .map((violation) => ` ${violation.describe()}`)
152
+ .sort()
153
+ .join('\n');
154
+ throw new LevelViolationError(`${source}: every dependency must sit on a STRICTLY lower level than its dependent, but `
155
+ + `${violations.length} edge(s) do not:\n${listed}\n`
156
+ + 'Fix: this is a defect in the topological stratification, not in the repo — the graph '
157
+ + 'was sorted immediately before this check, so the levels and the edges cannot legally '
158
+ + 'disagree. Report it with the edges above.');
159
+ }
160
+ /**
161
+ * Recover one ordered cycle from a strongly connected component: walk forward inside the
162
+ * component until a node repeats, then return the closed chain from that repeat onward. A
163
+ * one-node component only reaches here when it carries a self-edge, which yields `x -> x`.
164
+ */
165
+ pathWithin(graph, members) {
166
+ const start = [...members].sort()[0];
167
+ const path = [start];
168
+ const seen = new Map([[start, 0]]);
169
+ let current = start;
170
+ for (;;) {
171
+ const next = (graph[current] ?? []).filter((dep) => members.has(dep)).sort()[0];
172
+ const at = seen.get(next);
173
+ if (at !== undefined)
174
+ return [...path.slice(at), next];
175
+ seen.set(next, path.length);
176
+ path.push(next);
177
+ current = next;
178
+ }
179
+ }
180
+ }
181
+ exports.ProjectCycleDetector = ProjectCycleDetector;
182
+ //# sourceMappingURL=graph-cycles.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graph-cycles.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-cycles.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Project cycle detection for the compile-time dependency graph.\n *\n * The architecture graph is a BUILD graph: a cycle in it is not a style problem, it is a graph that\n * cannot be built in any order, and the level numbers every rendered row is keyed on are meaningless\n * the moment one exists.\n *\n * `sortGraphTopologically` already refuses to stratify a cyclic graph, but it reports exactly ONE\n * cycle plus an undifferentiated \"among: a, b, c, d, e\" list — so a repo with three independent\n * cycles fixes one, re-runs, finds another, three times. This enumerates ALL of them up front, each\n * as a concrete `a -> b -> c -> a` chain of PROJECT KEYS, so one run names everything to break.\n *\n * IT OPERATES ON PROJECT KEYS, NEVER ON DISPLAY NAMES. A workspace may hold both\n * `@scope/public-api` and `public-api`; running detection on scope-stripped names would fuse them\n * and report their perfectly legal L6 → L0 edge as a self-loop cycle. See graph-names.ts.\n *\n * Self-loops count: a project listing itself in `dependsOn` is the degenerate cycle and fails too.\n */\n\nimport { findRuntimeCycles } from './runtime-cycles';\n\n/** A directed dependency graph keyed on PROJECT KEYS: `graph[project]` is what `project` depends on. */\nexport type ProjectAdjacency = Record<string, string[]>;\n\n/**\n * The one field the level assertion reads off a project entry.\n *\n * Spelled here rather than importing `EnhancedGraph` from graph-sorter on purpose: graph-sorter\n * imports THIS module (it reports its Kahn stall through the detector), and even a type-only import\n * back closes a file-import cycle the build refuses. `EnhancedGraph` satisfies this structurally.\n */\nexport class ProjectLevel {\n constructor(public readonly level: number) {}\n}\n\n/** Any project map carrying levels — what {@link ProjectCycleDetector.levelsOf} reads. */\nexport type LevelledProjects = Record<string, ProjectLevel>;\n\n/** One cycle, as the ordered chain of project keys that closes it (first key repeated at the end). */\nexport class ProjectCycle {\n constructor(public readonly path: string[]) {}\n\n /** `a -> b -> c -> a`, the form a reader can act on directly. */\n describe(): string {\n return this.path.join(' -> ');\n }\n}\n\n/** One edge whose direction contradicts the levels assigned to its endpoints. */\nexport class LevelViolation {\n constructor(\n public readonly from: string,\n public readonly fromLevel: number,\n public readonly to: string,\n public readonly toLevel: number,\n ) {}\n\n describe(): string {\n return `${this.from} (L${this.fromLevel}) -> ${this.to} (L${this.toLevel})`;\n }\n}\n\n/** Thrown when the compile-time project graph is not a DAG. */\nexport class CircularProjectDependencyError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'CircularProjectDependencyError';\n }\n}\n\n/** Thrown when a freshly stratified graph contains an edge that its own levels forbid. */\nexport class LevelViolationError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'LevelViolationError';\n }\n}\n\nexport class ProjectCycleDetector {\n /**\n * Every cycle in the graph, deterministically ordered.\n *\n * Tarjan (shared with the runtime graph's detector — one SCC implementation, two callers) gives\n * the strongly connected components; each component is then walked to recover an actual ordered\n * path, because \"these five projects are tangled\" is not something a reader can act on and\n * `a -> b -> c -> a` is.\n */\n find(graph: ProjectAdjacency): ProjectCycle[] {\n const cycles = findRuntimeCycles(graph).map(\n (scc): ProjectCycle => new ProjectCycle(this.pathWithin(graph, new Set(scc.services))));\n return cycles.sort((a: ProjectCycle, b: ProjectCycle): number =>\n a.describe().localeCompare(b.describe()));\n }\n\n /**\n * Fail the generation on any cycle, naming ALL of them.\n *\n * @param source what the graph was read from, so the message says WHERE to go fix it.\n */\n assertAcyclic(graph: ProjectAdjacency, source: string): void {\n const cycles = this.find(graph);\n if (cycles.length === 0) return;\n const listed = cycles.map((cycle: ProjectCycle): string => ` ${cycle.describe()}`).join('\\n');\n const plural = cycles.length === 1 ? 'cycle' : 'cycles';\n throw new CircularProjectDependencyError(\n `${source}: the project dependency graph is a BUILD graph and must be acyclic, but it `\n + `contains ${cycles.length} ${plural}:\\n${listed}\\n`\n + 'Fix: break each chain above by removing one edge in it — move the shared code into a '\n + 'lower-level library both sides depend on, or invert the dependency behind an '\n + 'api-lib contract. Every project on a chain is unbuildable until it is broken.');\n }\n\n /**\n * The level each project was stratified onto — the input {@link assertLevelsDescend} checks\n * against. Only `level` is read off each entry.\n */\n levelsOf(graph: LevelledProjects): Map<string, number> {\n const levels = new Map<string, number>();\n for (const project of Object.keys(graph)) levels.set(project, graph[project].level);\n return levels;\n }\n\n /**\n * Edges that contradict the levels assigned to their endpoints.\n *\n * `sortGraphTopologically` stratifies so that every dependency sits STRICTLY below its\n * dependent, so on a freshly sorted graph this can only fire if the stratification itself broke\n * — it is an assertion about the sorter, not a rule about repos. Callers must therefore only run\n * it against a graph they just sorted, never against a possibly-stale committed file.\n */\n findLevelViolations(graph: ProjectAdjacency, levelOf: Map<string, number>): LevelViolation[] {\n const violations: LevelViolation[] = [];\n for (const project of Object.keys(graph)) {\n const fromLevel = levelOf.get(project);\n if (fromLevel === undefined) continue;\n for (const dep of graph[project] ?? []) {\n const toLevel = levelOf.get(dep);\n if (toLevel === undefined || fromLevel > toLevel) continue;\n violations.push(new LevelViolation(project, fromLevel, dep, toLevel));\n }\n }\n return violations;\n }\n\n /** Fail loudly when a just-sorted graph disagrees with its own levels. */\n assertLevelsDescend(graph: ProjectAdjacency, levelOf: Map<string, number>, source: string): void {\n const violations = this.findLevelViolations(graph, levelOf);\n if (violations.length === 0) return;\n const listed = violations\n .map((violation: LevelViolation): string => ` ${violation.describe()}`)\n .sort()\n .join('\\n');\n throw new LevelViolationError(\n `${source}: every dependency must sit on a STRICTLY lower level than its dependent, but `\n + `${violations.length} edge(s) do not:\\n${listed}\\n`\n + 'Fix: this is a defect in the topological stratification, not in the repo — the graph '\n + 'was sorted immediately before this check, so the levels and the edges cannot legally '\n + 'disagree. Report it with the edges above.');\n }\n\n /**\n * Recover one ordered cycle from a strongly connected component: walk forward inside the\n * component until a node repeats, then return the closed chain from that repeat onward. A\n * one-node component only reaches here when it carries a self-edge, which yields `x -> x`.\n */\n private pathWithin(graph: ProjectAdjacency, members: Set<string>): string[] {\n const start = [...members].sort()[0];\n const path: string[] = [start];\n const seen = new Map<string, number>([[start, 0]]);\n let current = start;\n for (;;) {\n const next = (graph[current] ?? []).filter((dep: string): boolean => members.has(dep)).sort()[0];\n const at = seen.get(next);\n if (at !== undefined) return [...path.slice(at), next];\n seen.set(next, path.length);\n path.push(next);\n current = next;\n }\n }\n}\n"]}
@@ -1,17 +1,60 @@
1
1
  /**
2
2
  * Graph Names
3
3
  *
4
- * Shared helper for turning a scoped project name into the short display name
5
- * used everywhere in the visualization (graph box titles, the lock-control
6
- * options, and the responsibilities cards). Kept in its own class so both
7
- * GraphVisualizer and ResponsibilitiesRenderer can depend on it without a
8
- * circular dependency between them.
4
+ * The two names a project has in the visualization, kept apart on purpose:
5
+ *
6
+ * - its NODE ID — the identity every DOT node, edge endpoint, `rank=same` member, lock-dropdown
7
+ * value and responsibilities `data-node` attribute is keyed on. It is the project KEY, verbatim.
8
+ * - its SHORT NAME — the human-facing label, with the `@scope/` prefix stripped.
9
+ *
10
+ * They used to be the same string, and that was a silent, badly-wrong-picture bug. A workspace may
11
+ * legitimately hold `@scope/public-api` (an L0 contract lib) AND `public-api` (the L6 server that
12
+ * serves it) — two DISTINCT projects that strip to the same short name. Keying the DOT on the short
13
+ * name fused them into one node, and graphviz UNIONS any two `rank=same` sets sharing a node, so
14
+ * L0's rank and L6's rank became one rank: the entire L0 band rendered on the L6 row, the invisible
15
+ * anchor chain that pins level order became unsatisfiable (it demands L0 strictly below L6 while the
16
+ * union says they are equal) and was discarded, and the server's dependency on the contract lib drew
17
+ * as a self-loop on the fused box.
18
+ *
19
+ * So identity is the project key and nothing else — collision is structurally impossible now rather
20
+ * than merely unlikely — and {@link GraphNames.assertUniqueNodeIds} asserts that, so a future change
21
+ * that reintroduces a lossy id fails generation loudly instead of drawing a wrong graph in silence.
22
+ *
23
+ * Kept in its own class so GraphVisualizer and ResponsibilitiesRenderer can both depend on it
24
+ * without a circular dependency between them.
9
25
  */
26
+ /** One project key and the level it sits on — what a collision report has to name. */
27
+ export declare class NodeIdOwner {
28
+ readonly projectKey: string;
29
+ readonly level: number;
30
+ constructor(projectKey: string, level: number);
31
+ }
32
+ /** Thrown when two distinct project keys would be drawn as the same DOT node. */
33
+ export declare class NodeIdCollisionError extends Error {
34
+ constructor(message: string);
35
+ }
10
36
  export declare class GraphNames {
11
37
  /**
12
- * Remove scope from name for display
38
+ * The DOT/DOM identity of a project: its project KEY, unchanged.
39
+ *
40
+ * This is deliberately a named method rather than an inlined `project` at ten call sites — the
41
+ * node id is one decision, and it has to be made in one place or the DOT, the lock dropdown and
42
+ * the responsibilities cards drift apart and the hover/lock wiring silently stops matching.
43
+ */
44
+ getNodeId(projectKey: string): string;
45
+ /**
46
+ * The human-facing display name: scope stripped.
13
47
  * '@scope/name' → 'name'
14
48
  * 'name' → 'name'
49
+ *
50
+ * LABELS ONLY. Never use this as an identity — see this file's header for what that cost.
15
51
  */
16
52
  getShortName(name: string): string;
53
+ /**
54
+ * Assert that every project draws as its own node.
55
+ *
56
+ * Fails LOUDLY, naming both colliding project keys and their levels, because the failure mode it
57
+ * guards is a graph that renders happily and says something false.
58
+ */
59
+ assertUniqueNodeIds(owners: NodeIdOwner[]): void;
17
60
  }