function factory( options ) {
	return linter;

	/**
	* Validates HTML section hierarchy in README files according to stdlib conventions.
	*
	* @private
	* @param {Node} tree - abstract syntax tree (AST)
	* @param {File} file - virtual file
	* @returns {void}
	*/
	function linter( tree, file ) {
		var requiredRootSections;
		var requiredCSections;
		var sectionStructure;
		var sectionsFound;
		var sectionStack;
		var missingRoot;
		var missingC;
		var schema;
		var msg;
		var i;

		debug( 'Linting file: %s', file.path || '' );
		schema = options.schema;

		// Initialize section tracking:
		sectionStack = [];
		sectionStructure = [];

		// Keep track of sections at different levels:
		sectionsFound = {
			'root': {},
			'c': {}
		};

		// Visit all HTML nodes to build section structure:
		visit( tree, 'html', visitor );

		// Log all sections found for debugging:
		debug( 'Root sections found: %j', keys( sectionsFound.root ) );
		if ( sectionsFound.root.c ) {
			debug( 'C sections found: %j', keys( sectionsFound.c ) );
		}

		// After visiting all nodes, validate against schema:
		requiredRootSections = schema.root.required || [];

		// Check for missing required root sections:
		missingRoot = [];
		for ( i = 0; i < requiredRootSections.length; i++ ) {
			if ( !sectionsFound.root[ requiredRootSections[ i ] ] ) {
				missingRoot.push( requiredRootSections[ i ] );
			}
		}

		if ( missingRoot.length > 0 ) {
			msg = 'Missing required root-level sections: `' + missingRoot.join( '`, `' ) + '`. Required sections are: `' + requiredRootSections.join( '`, `' ) + '`.   missing-required-sections';
			debug( msg );
			file.message( msg, tree );
		}

		// If 'c' section exists, check its requirements:
		if ( sectionsFound.root.c ) {
			requiredCSections = ( schema.c ) ? ( schema.c.required || [] ) : [];

			// Check for missing required C sections:
			missingC = [];
			for ( i = 0; i < requiredCSections.length; i++ ) {
				if ( !sectionsFound.c[ requiredCSections[ i ] ] ) {
					missingC.push( requiredCSections[ i ] );
				}
			}

			if ( missingC.length > 0 ) {
				msg = 'Missing required sections in "c" section: `' + missingC.join( '`, `' ) + '`. Required C sections are: `' + requiredCSections.join( '`, `' ) + '`.   missing-required-c-sections';
				debug( msg );
				file.message( msg, sectionsFound.root.c.node );
			}
		}

		debug( 'Finished linting: %s', file.path || '' );

		/**
		* Callback invoked upon finding a matching node.
		*
		* @private
		* @param {Object} node - AST node
		* @returns {void}
		*/
		function visitor( node ) {
			var currentSection;
			var sectionMatch;
			var className;

			// Check if this is a section start tag:
			sectionMatch = RE_SECTION_START.exec( node.value );
			if ( sectionMatch ) {
				className = sectionMatch[ 1 ] || '';

				debug( 'Found section with class: %s', className );

				// Create section data object:
				currentSection = {
					'node': node,
					'name': className,
					'children': [],
					'parent': null
				};

				// Add to parent if there is one:
				if ( sectionStack.length > 0 ) {
					currentSection.parent = sectionStack[ sectionStack.length-1 ]; // eslint-disable-line max-len
					currentSection.parent.children.push( currentSection );

					// Record C-level section:
					if ( currentSection.parent.name === 'c' ) {
						sectionsFound.c[ className ] = currentSection;
					}
				} else {
					// This is a root section:
					sectionStructure.push( currentSection );
					sectionsFound.root[ className ] = currentSection;
				}

				// Push to stack:
				sectionStack.push( currentSection );
			}
			// Check if this is a section end tag:
			else if ( RE_SECTION_END.test( node.value ) && sectionStack.length > 0 ) { // eslint-disable-line max-len
				sectionStack.pop();
			}
		}
	}
}
Returns a linter function.

@private
@param {Object} options - linter options
@param {Object} [options.schema] - section schema defining required and optional sections
@returns {Function} linter function