The incorrect error code is because ValidatingFilter.reportValidationError() unconditionally sets XTTE1510 even if the error object already contains a more specific code.
The failure to report decent messages is because because validationContext.getInvalidityHandler() returns an InvalidityReportGenerator rather than an InvalidityHandlerWrappingErrorListener, which should be the default. It's not immediately clear why this affects the ID validation errors and not other kinds of validity error.
This is set up by EnterpriseConfiguration.prepareValidationReporting(), which has the logic:
public void prepareValidationReporting(XPathContext context, ParseOptions options) throws XPathException {
if (context.getThreadManager() != null) {
// we're in a try/catch
Builder builder = context.getController().makeBuilder();
options.setInvalidityHandler(new InvalidityReportGeneratorEE(context.getConfiguration(), builder));
}
}
and context.getThreadManager() is not null. The rationale for this code is that if we do validation in a try/catch, we want to save validation errors and not report them if the validation error is caught. But the inference in the comment is wrong: in this test case, we are not in a try/catch, and yet context.getThreadManager() is non-null.
The ThreadManager is being created by Controller.callTemplate() - that is, it is being created unconditionally when the transformation starts with a named template. But it also happens on other invocation paths, e.g. Controller.applyTemplates() and Controller.transformDocument(). It looks to me as if the existence of the ThreadManager was at one time indicative that we were in a try/catch, but this is no longer the case, and we need a better test.