@webpieces/nx-webpieces-rules 0.4.653 → 0.4.655
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 +6 -6
- package/src/branch-identity-inputs.d.ts +27 -0
- package/src/branch-identity-inputs.js +33 -0
- package/src/branch-identity-inputs.js.map +1 -0
- package/src/executors/generate/executor.js +12 -0
- package/src/executors/generate/executor.js.map +1 -1
- package/src/executors/validate-no-architecture-cycles/executor.js +12 -7
- package/src/executors/validate-no-architecture-cycles/executor.js.map +1 -1
- package/src/lib/graph-cycles.d.ts +96 -0
- package/src/lib/graph-cycles.js +182 -0
- package/src/lib/graph-cycles.js.map +1 -0
- package/src/lib/graph-names.d.ts +49 -6
- package/src/lib/graph-names.js +88 -7
- package/src/lib/graph-names.js.map +1 -1
- package/src/lib/graph-responsibilities.d.ts +1 -1
- package/src/lib/graph-responsibilities.js +6 -2
- package/src/lib/graph-responsibilities.js.map +1 -1
- package/src/lib/graph-sorter.js +7 -39
- package/src/lib/graph-sorter.js.map +1 -1
- package/src/lib/graph-visualizer.d.ts +10 -0
- package/src/lib/graph-visualizer.js +33 -8
- package/src/lib/graph-visualizer.js.map +1 -1
- package/src/lib/runtime-cycles.d.ts +5 -2
- package/src/lib/runtime-cycles.js +5 -2
- package/src/lib/runtime-cycles.js.map +1 -1
- package/src/lib/runtime-visualizer.js +29 -14
- package/src/lib/runtime-visualizer.js.map +1 -1
- package/src/plugin.js +8 -1
- package/src/plugin.js.map +1 -1
- package/src/validation-targets.js +4 -3
- package/src/validation-targets.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/nx-webpieces-rules",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.655",
|
|
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.
|
|
22
|
-
"@webpieces/code-rules": "0.4.
|
|
23
|
-
"@webpieces/eslint-rules": "0.4.
|
|
24
|
-
"@webpieces/pr-gate": "0.4.
|
|
25
|
-
"@webpieces/rules-config": "0.4.
|
|
21
|
+
"@webpieces/ai-hook-rules": "0.4.655",
|
|
22
|
+
"@webpieces/code-rules": "0.4.655",
|
|
23
|
+
"@webpieces/eslint-rules": "0.4.655",
|
|
24
|
+
"@webpieces/pr-gate": "0.4.655",
|
|
25
|
+
"@webpieces/rules-config": "0.4.655",
|
|
26
26
|
"madge": "8.0.0"
|
|
27
27
|
},
|
|
28
28
|
"peerDependencies": {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { TargetConfiguration } from '@nx/devkit';
|
|
2
|
+
/**
|
|
3
|
+
* The BRANCH IDENTITY, as nx task-hash inputs. Append these to any CACHED target that can be turned off by
|
|
4
|
+
* `turnOffRuleWhileOnBranch`.
|
|
5
|
+
*
|
|
6
|
+
* WHY A CACHED RULE TARGET NEEDS THEM: the branch hatch makes a rule's verdict depend on which branch you
|
|
7
|
+
* are on, and nothing about the branch is otherwise in the hash. So a hatched branch runs `<project>:ci`
|
|
8
|
+
* green and caches it under hash H; a later, UNRELATED PR that did not touch that project's files hashes to
|
|
9
|
+
* the same H and REPLAYS the hatched green — the relaxation escapes the one branch that opted into it,
|
|
10
|
+
* which is the single property the hatch is supposed to guarantee.
|
|
11
|
+
*
|
|
12
|
+
* `{workspaceRoot}/webpieces.config.json` in `sharedGlobals` (nx.json) is the other half and neither half
|
|
13
|
+
* is sufficient alone: the config file makes EDITING a hatch bust the cache, but two branches sharing one
|
|
14
|
+
* unedited config file still hash identically — which is exactly the leak above.
|
|
15
|
+
*
|
|
16
|
+
* DELIBERATELY NOT IN `sharedGlobals`. Putting a branch-varying value there would make EVERY task hash
|
|
17
|
+
* branch-unique and destroy cross-branch cache reuse fleet-wide (CLAUDE.md's measured ~3.2x contention cost
|
|
18
|
+
* is what that buys you). Scoped to the rule-running targets, `build`/`test`/`lint` keep sharing.
|
|
19
|
+
*
|
|
20
|
+
* The two vars are the same pair `getCurrentBranch()` reads, in the same order, so the hash keys off
|
|
21
|
+
* exactly the values the skip decision keys off. A plain `git rev-parse` is deliberately NOT used: nx
|
|
22
|
+
* inputs must be cheap and pure, and on the CI checkout the vars are the authoritative answer anyway.
|
|
23
|
+
*
|
|
24
|
+
* These are object literals because that is the nx INPUT DSL, not a webpieces data structure — the same
|
|
25
|
+
* form the `{'runtime': ...}` inputs in validation-targets.ts already use.
|
|
26
|
+
*/
|
|
27
|
+
export declare const BRANCH_IDENTITY_INPUTS: NonNullable<TargetConfiguration['inputs']>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BRANCH_IDENTITY_INPUTS = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* The BRANCH IDENTITY, as nx task-hash inputs. Append these to any CACHED target that can be turned off by
|
|
6
|
+
* `turnOffRuleWhileOnBranch`.
|
|
7
|
+
*
|
|
8
|
+
* WHY A CACHED RULE TARGET NEEDS THEM: the branch hatch makes a rule's verdict depend on which branch you
|
|
9
|
+
* are on, and nothing about the branch is otherwise in the hash. So a hatched branch runs `<project>:ci`
|
|
10
|
+
* green and caches it under hash H; a later, UNRELATED PR that did not touch that project's files hashes to
|
|
11
|
+
* the same H and REPLAYS the hatched green — the relaxation escapes the one branch that opted into it,
|
|
12
|
+
* which is the single property the hatch is supposed to guarantee.
|
|
13
|
+
*
|
|
14
|
+
* `{workspaceRoot}/webpieces.config.json` in `sharedGlobals` (nx.json) is the other half and neither half
|
|
15
|
+
* is sufficient alone: the config file makes EDITING a hatch bust the cache, but two branches sharing one
|
|
16
|
+
* unedited config file still hash identically — which is exactly the leak above.
|
|
17
|
+
*
|
|
18
|
+
* DELIBERATELY NOT IN `sharedGlobals`. Putting a branch-varying value there would make EVERY task hash
|
|
19
|
+
* branch-unique and destroy cross-branch cache reuse fleet-wide (CLAUDE.md's measured ~3.2x contention cost
|
|
20
|
+
* is what that buys you). Scoped to the rule-running targets, `build`/`test`/`lint` keep sharing.
|
|
21
|
+
*
|
|
22
|
+
* The two vars are the same pair `getCurrentBranch()` reads, in the same order, so the hash keys off
|
|
23
|
+
* exactly the values the skip decision keys off. A plain `git rev-parse` is deliberately NOT used: nx
|
|
24
|
+
* inputs must be cheap and pure, and on the CI checkout the vars are the authoritative answer anyway.
|
|
25
|
+
*
|
|
26
|
+
* These are object literals because that is the nx INPUT DSL, not a webpieces data structure — the same
|
|
27
|
+
* form the `{'runtime': ...}` inputs in validation-targets.ts already use.
|
|
28
|
+
*/
|
|
29
|
+
exports.BRANCH_IDENTITY_INPUTS = [
|
|
30
|
+
{ env: 'GITHUB_HEAD_REF' },
|
|
31
|
+
{ env: 'WEBPIECES_BRANCH' },
|
|
32
|
+
];
|
|
33
|
+
//# sourceMappingURL=branch-identity-inputs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"branch-identity-inputs.js","sourceRoot":"","sources":["../../../../../packages/tooling/nx-webpieces-rules/src/branch-identity-inputs.ts"],"names":[],"mappings":";;;AAEA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACU,QAAA,sBAAsB,GAA+C;IAC9E,EAAE,GAAG,EAAE,iBAAiB,EAAE;IAC1B,EAAE,GAAG,EAAE,kBAAkB,EAAE;CAC9B,CAAC","sourcesContent":["import { TargetConfiguration } from '@nx/devkit';\n\n/**\n * The BRANCH IDENTITY, as nx task-hash inputs. Append these to any CACHED target that can be turned off by\n * `turnOffRuleWhileOnBranch`.\n *\n * WHY A CACHED RULE TARGET NEEDS THEM: the branch hatch makes a rule's verdict depend on which branch you\n * are on, and nothing about the branch is otherwise in the hash. So a hatched branch runs `<project>:ci`\n * green and caches it under hash H; a later, UNRELATED PR that did not touch that project's files hashes to\n * the same H and REPLAYS the hatched green — the relaxation escapes the one branch that opted into it,\n * which is the single property the hatch is supposed to guarantee.\n *\n * `{workspaceRoot}/webpieces.config.json` in `sharedGlobals` (nx.json) is the other half and neither half\n * is sufficient alone: the config file makes EDITING a hatch bust the cache, but two branches sharing one\n * unedited config file still hash identically — which is exactly the leak above.\n *\n * DELIBERATELY NOT IN `sharedGlobals`. Putting a branch-varying value there would make EVERY task hash\n * branch-unique and destroy cross-branch cache reuse fleet-wide (CLAUDE.md's measured ~3.2x contention cost\n * is what that buys you). Scoped to the rule-running targets, `build`/`test`/`lint` keep sharing.\n *\n * The two vars are the same pair `getCurrentBranch()` reads, in the same order, so the hash keys off\n * exactly the values the skip decision keys off. A plain `git rev-parse` is deliberately NOT used: nx\n * inputs must be cheap and pure, and on the CI checkout the vars are the authoritative answer anyway.\n *\n * These are object literals because that is the nx INPUT DSL, not a webpieces data structure — the same\n * form the `{'runtime': ...}` inputs in validation-targets.ts already use.\n */\nexport const BRANCH_IDENTITY_INPUTS: NonNullable<TargetConfiguration['inputs']> = [\n { env: 'GITHUB_HEAD_REF' },\n { env: 'WEBPIECES_BRANCH' },\n];\n"]}
|
|
@@ -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:
|
|
30
|
-
|
|
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
|
-
|
|
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;;
|
|
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"]}
|