docs: function getReporter(out, style) {
  
  	var repAccent = style.accent('/');
  	var repProto = style.accent('://');
  
  	// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  
  	function tweakURI(str) {
  		return str.split(/:\/\//).map(function (str) {
  			return str.replace(/\//g, repAccent);
  		}).join(repProto);
  	}
  
  	function tweakMessage(str) {
  		return style.warning(str.charAt(0).toLowerCase() + str.substr(1));
  	}
  
  	function tweakPath(str) {
  		return str.replace(/\//g, style.accent('/'));
  	}
  
  	// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  
  	// best-effort
  	function extractSchemaLabel(schema, limit) {
  		limit = typeof limit === 'undefined' ? strimLimit : limit;
  		var label = '';
  		if (schema.id) {
  			label = style.accent(schema.id);
  		}
  		if (schema.title) {
  			label += style.accent(label ? ' (' + schema.title + ')' : style.accent(schema.title));
  		}
  
  		if (!label) {
  			if (schema.description) {
  				label = style.accent('<no id>') + ' ' + valueStrim(schema.description, limit);
  			}
  			else {
  				label = style.accent('<no id>') + ' ' + valueStrim(schema, limit);
  			}
  		}
  		return label;
  	}
  
  	// best-effort
  	function extractCTXLabel(test, limit) {
  		limit = typeof limit === 'undefined' ? strimLimit : limit;
  		var label;
  		if (test.label) {
  			label = style.accent(test.label);
  		}
  		if (!label) {
  			label = style.accent('<no label>') + ' ' + valueStrim(test.value, limit);
  		}
  		return label;
  	}
  
  	// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  
  	function reportResult(test, indent) {
  		assertTest(test);
  
  		if (test.result.valid) {
  			if (test.failOnMissing && test.result.missing && test.result.missing.length > 0) {
  				reportFailed(test, indent);
  			}
  			else {
  				reportSuccess(test);
  			}
  		}
  		else {
  			reportFailed(test, indent);
  		}
  		reportMissing(test, indent);
  	}
  
  	function reportSuccess(test) {
  		out.writeln(style.success('>> ') + 'success ' + extractCTXLabel(test));
  		out.writeln(style.success('>> ') + extractSchemaLabel(test.schema));
  	}
  
  	function reportFailed(test, indent) {
  		out.writeln(style.error('>> ') + 'failed ' + style.error(test.label));
  		out.writeln(style.error('!= ') + extractSchemaLabel(test.schema));
  
  		if (test.result.errors) {
  			test.result.errors.forEach(function (err) {
  				reportError(test, err, indent, indent);
  			});
  		}
  		else if (test.result.error) {
  			reportError(test, test.result.error, indent, indent);
  		}
  	}
  
  	function reportMissing(test, indent) {
  		if (test.result.missing && test.result.missing.length > 0) {
  			out.writeln('   ' + style.warning('missing ' + pluralise('schema', test.result.missing.length) + ':') + ' ');
  			test.result.missing.forEach(function (missing) {
  				out.writeln(indent + style.error(' - ') + valueStrim(missing));
  			});
  		}
  	}
  
  	function reportError(test, error, indent, prefix, parentPath) {
  		assertTest(test);
  
  		var value = test.value;
  		if (typeof test.value === 'object') {
  			value = jsonpointer.get(test.value, error.dataPath);
  		}
  		var schemaValue = jsonpointer.get(test.schema, error.schemaPath);
  
  		indent = (typeof indent !== 'undefined' ? String(indent) : '   ');
  		prefix = (typeof prefix !== 'undefined' ? prefix : '');
  
  		if (error.message) {
  			out.writeln(prefix + tweakMessage(error.message));
  		}
  		else {
  			out.writeln(prefix + style.error('<no message>'));
  		}
  
  		if (typeof schemaValue !== 'undefined') {
  			out.writeln(prefix + indent + valueStrim(schemaValue) + style.accent(' -> ') + tweakPath(error.schemaPath));
  		}
  		else {
  			out.writeln(prefix + indent + tweakPath(error.schemaPath));
  		}
  
  		if ((typeof parentPath !== 'string' || parentPath !== error.dataPath)) {
  			if (error.dataPath !== '') {
  				out.writeln(prefix + indent + style.error(' > ') + tweakPath(error.dataPath));
  			}
  			else {
  				//out.writeln(prefix + indent + out.error(' > ') + '<root>');
  			}
  			if (typeof value === 'undefined') {
  				out.writeln(prefix + indent + style.error(' > ') + valueType(value));
  			}
  			else {
  				out.writeln(prefix + indent + style.error(' > ') + valueType(value) + style.error(' -> ') + valueStrim(value));
  			}
  		}
  
  		if (error.subErrors) {
  			error.subErrors.forEach(function (sub) {
  				// let's go deeper
  				reportError(test, sub, indent, prefix + indent, error.dataPath);
  			});
  		}
  	}
  
  	function reportTotals(numFailed, numPassed) {
  		var total = numFailed + numPassed;
  		if (numFailed > 0) {
  			out.writeln(style.error('>> ') + 'tv4 ' + (numPassed > 0 ? style.warning('validated ' + numPassed) + ', ' : '') + style.error('failed ' + numFailed) + ' of ' + style.error(total + ' ' + pluralise('value', total)));
  		}
  		else if (total === 0) {
  			//out.writeln('');
  			out.writeln(style.warning('>> ') + 'tv4 ' + style.warning('validated zero values'));
  		}
  		else {
  			//out.writeln('');
  			out.writeln(style.success('>> ') + 'tv4 ' + style.success('validated ' + numPassed) + ' of ' + style.success(total + ' ' + pluralise('value', total)));
  		}
  	}
  
  	function reportBulk(failed, passed, indent) {
  		if (!passed) {
  			passed = [];
  		}
  		indent = (typeof indent !== 'undefined' ? String(indent) : '   ');
  
  		var total = failed.length + passed.length;
  
  		//got some failures: print log and fail the task
  		if (failed.length > 0) {
  			failed.forEach(function (test, i) {
  				assertTest(test);
  
  				reportFailed(test, indent);
  
  				if (i < failed.length - 1) {
  					out.writeln('');
  				}
  			});
  			out.writeln('');
  		}
  		reportTotals(failed.length, passed.length);
  	}
  
  	return {
  		createTest: createTest,
  		checkTest: checkTest,
  		isTest: isTest,
  		assertTest: assertTest,
  
  		extractSchemaLabel: extractSchemaLabel,
  		extractCTXLabel: extractCTXLabel,
  
  		reportResult: reportResult,
  		reportSuccess: reportSuccess,
  		reportFailed: reportFailed,
  		reportError: reportError,
  		reportMissing: reportMissing,
  		reportBulk: reportBulk
  	};
  }