Refactor app validation errors: result-based parsing, structured error data#7126
Refactor app validation errors: result-based parsing, structured error data#7126ryancbahan merged 5 commits intomainfrom
Conversation
|
We detected some changes at Caution DO NOT create changesets for features which you do not wish to be included in the public changelog of the next CLI release. |
This stack of pull requests is managed by Graphite. Learn more about stacking. |
…r data Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…r data Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…r data Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
768157b to
3249375
Compare
|
|
||
| if (!unsafeTolerateErrors && !localApp.errors.isEmpty()) { | ||
| throw new AbortError(localApp.errors.toJSON()[0]!) | ||
| throw new AbortError(styledConfigurationError(localApp.errors.getErrors()[0]!)) |
There was a problem hiding this comment.
why only show the first error? Would be useful to show all validation errors.
There was a problem hiding this comment.
This is parity with what we do today on main actually. I agree we can broaden, but am avoiding behavioral changes alongside orthogonal features/refactors.
isaacroldan
left a comment
There was a problem hiding this comment.
Review assisted by pair-review
| throw new AbortError(newApp.errors.toJSON().join('\n')) | ||
| const errors = newApp.errors.getErrors() | ||
| throw new AbortError( | ||
| outputContent`${outputToken.errorText('Validation errors')}:\n\n${errors.map(formatConfigurationError).join('\n')}`, |
There was a problem hiding this comment.
there are many calls to formatConfigurationError, maybe the AppErrors class expose a getFormattedErrors getter to simplify that?
I've also seen this almost exact line in two places, here and in app-context.ts
(prepending the Validation error as a errorText). If we want to use the same format from different places would be nice to try to unify it somehow.
There was a problem hiding this comment.
I'm open to adjusting, but I definitely think that error formatting should be off of AppErrors/loader. Today, we make habit of passing around rendering concerns across the entire lifecycle and it adds complexity/mixes concerns too much. The decision of "how to render" should be as close to the render call as possible, allowing the "backend" to be concerned with data flow. The formatting of structured data to renderable info can still be abstracted when needed, IMO just not as part of the data flow itself.
There was a problem hiding this comment.
I get that reasoning, but we end up with the same formatting in multiple places, there must be a better way to do it? :thinking_face:
Seems like a case of consistency vs correctness? We are being more correct, but at the expense of duplicating code and potentially being less consistent if this transformations diverge?
I appreciate the push for correctness though! maybe we can meet in the middle :P
| }) | ||
| app = context.app | ||
| } catch (err) { | ||
| if (err instanceof AbortError && flags.json) { |
There was a problem hiding this comment.
I think this catch may be a bit too broad for the JSON contract.
linkedAppContext() does more than local config validation — it can also abort during link / remote app lookup / org fetch / spec fetch. For example, appFromIdentifiers() in services/context.ts throws AbortError for cases like “No app with client ID … found”.
So with this change, some operational/setup failures now look like:
{"valid": false, "errors": [{"message": "..."}]}which reads as “your config is invalid” even when the problem is actually linking/auth/remote resolution.
I think the --json fallback should probably only map actual local configuration failures into the validation result shape, and let unrelated AbortErrors bubble normally. Otherwise machine consumers may start treating non-validation failures as fixable TOML issues.
Apologies if we've litigated this already, but just want to confirm.
There was a problem hiding this comment.
I've updated the top pr to address this. It's a little hacky while we only have AbortError today, but I think we can continue to refine.
| const configuration = await parseConfigurationFile(configSchema, configurationPath, rawConfig) | ||
| const configResult = await parseConfigurationFile(configSchema, configurationPath, rawConfig) | ||
| if (configResult.errors) { | ||
| throw new AbortError(configResult.errors.map(formatConfigurationError).join('\n')) |
There was a problem hiding this comment.
This looks like a regression in the human-readable error path for app config loading.
Before, loadAppFromContext() threw an error that included the config file path:
throw new AbortError(
outputContent`Validation errors in ${outputToken.path(configurationPath)}:\n\n${...}`,
)Now it throws only:
throw new AbortError(configResult.errors.map(formatConfigurationError).join('\n'))But formatConfigurationError() doesn’t include error.file, only [path]: message or just message.
So for top-level app config parse/validation failures, we seem to lose which config file actually failed. In multi-config setups that feels materially less actionable than before.
Would it make sense to preserve the file context at this boundary, either by:
- keeping the
configurationPathwrapper here, or - making the formatter include file information when rendering for humans?
…r data Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…r data Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

What
Refactor app validation error handling from throw/catch with styled
OutputMessageto result-based parsing with a plainConfigurationErrordata interface.Why
Prep for
app config validate --json(see #7066, #7090, #7106). The original stack added structured error data alongside rendered messages, creating dual-track serialization in the loader. This PR takes the opposite approach: make errors pure data, let callers render. This gives callers more flexibility and removes the job of rendering/formatting from the data flow.How
ConfigurationErroris now a plain interface ({file, message, path?, code?}), not a class extendingAbortErrorparseConfigurationObject,parseConfigurationObjectAgainstSpecification,parseConfigurationFilereturn{data} | {errors: ConfigurationError[]}instead of throwingAppErrorsstoresConfigurationError[]withaddError,addErrors,getErrors(file?),isEmptyformatConfigurationErroris the single plain-text formatting functionoutputContent/outputToken) applied only at display boundaries:app-context.ts(AbortError),validate.ts(renderError),loader.ts(reloadApp)parseStructuredErrorsinerror-parsing.tsconverts Zod issues toParsedIssue[]with union-variant scoringNo user-facing behavior changes. CLI output preserves styling. Multi-error-per-file now preserved (previously silently overwritten by dictionary key).