Skip to content

fix(frontseat-plugin-backstage): GH-128 surface catalog decode errors - #241

Open
jbadeau wants to merge 2 commits into
mainfrom
fix/128-catalog-decode-errors
Open

fix(frontseat-plugin-backstage): GH-128 surface catalog decode errors#241
jbadeau wants to merge 2 commits into
mainfrom
fix/128-catalog-decode-errors

Conversation

@jbadeau

@jbadeau jbadeau commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

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

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)

@joaodinissf joaodinissf Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +114 to +117
// 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)

@joaodinissf joaodinissf Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@joaodinissf joaodinissf left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

backstage: catalog documents that fail to decode are silently dropped (e.g. tools: [node])

3 participants