fix(frontseat-plugin-backstage): GH-128 surface catalog decode errors - #241
fix(frontseat-plugin-backstage): GH-128 surface catalog decode errors#241jbadeau wants to merge 2 commits into
Conversation
Catalog documents that failed to decode (e.g. a field with the wrong YAML shape) were silently skipped, and whole-file load failures were demoted to a stderr warning — either way the entity just vanished from the catalog. Both now fail loud, naming the file and document index. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| if err != nil { | ||
| fmt.Fprintf(os.Stderr, "Warning: failed to load catalog %s: %v\n", catalogPath, err) | ||
| continue | ||
| return nil, fmt.Errorf("catalog %s: %w", catalogPath, err) |
There was a problem hiding this comment.
This makes it worse, not louder. The host swallows the error — the plugin.Entities(...) branch in Manager.DiscoverEntities (libs/frontseat-plugin-host/discover.go):
result, err := plugin.Entities(ctx, ws, matchedFiles)
if err != nil {
logger.Warn("plugin failed to create entities", ...)
continue
}Warn + continue. So one bad file means backstage returns nil and contributes zero entities, discovery still reports success, and the empty map is cached. One bad document costs the whole catalog instead of one entity.
Also folds a transient read error on file 3 into total catalog loss.
Fix: keep the per-file continue here and return a partial result alongside the error, or make the host treat an Entities error as fatal. The plugin alone cannot make this loud.
There was a problem hiding this comment.
Fixed in 994fe9f. The host no longer swallows the error: the plugin.Entities branch in Manager.DiscoverEntities now returns fmt.Errorf("plugin %s failed to create entities in %s: %w", ...) instead of warn-and-continue, so a genuinely broken catalog fails discovery loudly rather than caching an empty map as success. That's the half the plugin couldn't fix on its own — so Fixes #128 is now accurate.
Paired with the plugin change so a transient read error / syntax error is fatal (loud), while a wrong-shape document is reported-and-skipped without taking its siblings down (see the other thread).
Generated by Claude Code
| // A document that fails to decode is a user error (e.g. a field | ||
| // with the wrong shape) — surface it instead of silently dropping | ||
| // the entity from the catalog. | ||
| return nil, fmt.Errorf("document %d: %w", doc, err) |
There was a problem hiding this comment.
errors.Is(err, io.EOF) replacing the err.Error() == "EOF" string compare is a real fix on its own — good. So is terminating the loop: on a YAML syntax error the previous code spun forever, because Decode returns the identical error on every call and never reaches EOF.
But aborting the whole file on any decode error also drops the valid documents beside the bad one, which is the opposite of what #128 asks for. #128 wants the bad entity to stop vanishing silently; it does not ask for its siblings to vanish too.
Suggest: warn naming file and document index, keep the successfully decoded entities, and continue — but only for the error class where continuing is safe. The discriminator has to be the error type: continue on a *yaml.TypeError, where the decoder advances to the next document, and stop on anything else. "Did the entity come out populated?" cannot discriminate, because a syntax error also yields an empty entity.
There was a problem hiding this comment.
Done in 994fe9f — discriminating on error type exactly as you laid out:
var typeErr *yaml.TypeError
if errors.As(derr, &typeErr) {
skipped = append(skipped, fmt.Sprintf("backstage: catalog %s document %d has an invalid shape, skipping: %v", path, doc, derr))
continue
}
return entities, skipped, fmt.Errorf("document %d: %w", doc, derr)continue on *yaml.TypeError (the decoder has advanced, siblings survive), abort on anything else (syntax error, which never advances or reaches EOF). Kept errors.Is(err, io.EOF). The skip is surfaced via plugin stderr (backstage:-prefixed), so the bad document stops vanishing silently without dragging its siblings with it. TestDecodeCatalogDocuments_TypeErrorKeepsSiblings pins the bad-doc-plus-valid-sibling case; TestLoadEntitiesFromFiles_SyntaxErrorSurfaces pins the abort path.
Also extended to both catalog rules — catalog-schema and catalog-best-practices now emit a violation on an unparseable catalog-info.yaml instead of dropping the error, so it's visible to conformance as well as discovery.
Generated by Claude Code
There was a problem hiding this comment.
Requesting changes — this is a regression on the real path.
Two inline. The core one: the host warn-and-continues on an Entities error (the plugin.Entities branch in Manager.DiscoverEntities), so returning early means backstage contributes zero entities while discovery still reports success. One bad document used to cost one entity; now it costs the whole catalog.
Worth keeping: errors.Is(err, io.EOF) is right, and aborting genuinely terminates a real hang — the previous loop spun forever on a YAML syntax error, since Decode returns the same error on every call and never reaches EOF. The fix needs to preserve that while not discarding valid documents.
The same silent continue survives in both catalog rules — each calls loadEntityFromFile and drops the error on the floor (rule_catalog_schema.go, rule_catalog_best_practices.go), so an unparseable catalog file is invisible to conformance as well as to discovery. Whatever this PR settles for an Entities error should apply at all three sites.
Fixes #128 is premature until the host stops swallowing the error.
This review was published with assistance from Claude.
…iblings The prior fix aborted the whole catalog file on any decode error, and the host swallowed the returned error (warn + continue), so one bad document reduced backstage to zero entities while discovery still reported success — a worse regression than the silent per-entity skip it replaced. - Plugin: discriminate on error type. A *yaml.TypeError (wrong-shape document) is reported by file + document index and skipped, and its valid siblings are kept — the decoder advances past a TypeError. Any other error (a YAML syntax error, on which Decode never advances or reaches EOF) still aborts, so the file fails loud instead of spinning. Plugin stderr is forwarded with a "backstage:" prefix, so the skip is surfaced, not silent. - Host: an Entities error in DiscoverEntities is now fatal instead of warn-and-continue, so a genuinely broken catalog fails discovery loudly rather than silently building against a partial catalog. - Both catalog rules (schema, best-practices) now emit a violation on an unparseable catalog-info.yaml instead of dropping the error, so a broken file is visible to conformance too. Tests cover the TypeError-keeps-siblings and syntax-error-aborts paths. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MYAgCD9HZHZWEuH4vF17p1
Fixes #128: a catalog document that failed to decode (the issue's example:
tools: [node]where a mapping is expected) was silently skipped mid-file, and a whole-file load failure was demoted to a stderr warning inside the plugin process — either way the entity silently vanished from the catalog.Both paths now return an error naming the file and the document index, so discovery fails loud instead of building against a partial catalog. Regression test included.
🤖 Generated with Claude Code