Skip to content

Feat: Declare plugin pipeline directions and complete ConfigSchema coverage - #847

Open
esnible wants to merge 1 commit into
rossoctl:mainfrom
esnible:plugin-directions-and-configschema
Open

Feat: Declare plugin pipeline directions and complete ConfigSchema coverage#847
esnible wants to merge 1 commit into
rossoctl:mainfrom
esnible:plugin-directions-and-configschema

Conversation

@esnible

@esnible esnible commented Sep 2, 2026

Copy link
Copy Markdown
Member

Makes plugin placement and config field metadata machine-readable, so config generators don't have to infer either from source.

Directions

New PluginCapabilities.Directions field. All 14 in-tree plugins declare their intended chain, published on /v1/plugins and /v1/pipeline as directions, and asserted by a test — so the Direction column in docs/plugin-catalog.md now has a source of truth instead of drifting.

Advisory, never fatal. No plugin enforces direction at runtime (opa, the one that cares, only branches on pctx.Direction), so a misplaced plugin is a probable misconfiguration rather than a guaranteed one, and failing the boot would break configs that work today. A mismatch logs a startup WARN (plugins.WarnPluginDirections, called beside the existing WarnEmptyPipelines) and shows an advisory in abctl. Nil Directions means unconstrained, so out-of-tree plugins are unaffected.

Two naming/typing decisions:

  • The wire field is plural. direction (singular) already means "the chain this configured instance sits in" and is untouched; directions is the type-level set a plugin supports.
  • It's []string, not []Direction, because Direction.UnmarshalJSON decodes any unknown value to Inbound without erroring — fine for a single value, but on a slice that turns a future third direction into a false "inbound" claim.

ConfigSchema

mcp-parser, opa, session-budget and litellm-budget-track were Configurable but not SchemaProvider, so /v1/plugins reported no field metadata and abctl rendered them as bare names. All four now implement it; mcp-parser and opa also gained field annotations, with defaults and required flags taken from their applyDefaults/Configure rather than guessed.

a2a-parser and inference-parser are excluded — they have no config at all, and pipeline/schema.go names them as legitimate omissions. (The original ask said six plugins; four is the real count, and two of those were already annotated.)

Also adds a "number" schema type for floats: litellm-budget-track is the first plugin with float config, so without it max_budget and five per-token rates would publish as "unknown" and render as quoted empty strings in templates.

abctl

Templates carry a # chain: line per plugin, and the pre-apply validator flags a plugin pasted into a chain it doesn't declare. ValidationError gains a Severity so advisories render under their own banner — folding them into the existing one would make its "framework reload will reject" claim false.

Testing

go build, go vet, go test clean across all five modules (cpex type-checked with -tags cpex).

I verified the two highest-risk new tests actually fail when the code is broken. The clone-isolation test initially passed with Directions removed from cloneCatalog — it hit its own t.Skip, which is exactly the silent-drop bug it exists to catch; it now fails and names the culprit.

End-to-end: the WARN fires with plugin, both directions and position, and the pipeline still builds; /v1/plugins reports directions for all 11 default-build plugins and number for the six float fields.

Assisted-By: Claude (Anthropic AI) noreply@anthropic.com

Summary by CodeRabbit

  • New Features

    • Plugin catalogs and pipeline details now show supported inbound and outbound directions.
    • Configuration templates include chain guidance and use 0 for numeric fields.
    • Plugin configuration schemas now expose richer field metadata, including floating-point fields as numbers.
  • Bug Fixes

    • Direction mismatches are identified as advisories rather than blocking valid configuration changes.
    • Startup warnings highlight plugins placed in unsupported pipeline directions.
  • Documentation

    • Updated API, catalog, and configuration guidance to explain direction metadata and advisory behavior.

…verage

Make plugin placement and config metadata machine-readable, so config
generators no longer have to infer either from source or from a
hand-maintained table.

Directions (new PluginCapabilities field):

Every plugin has an intended chain -- jwt-validation is inbound,
token-exchange outbound, opa both -- but that lived only in the Direction
column of docs/plugin-catalog.md. All 14 in-tree plugins now declare it
as Capabilities().Directions, published on /v1/plugins and /v1/pipeline
as `directions`, and asserted by a test so the docs table has a source
of truth.

Advisory, never fatal: no plugin enforces direction at runtime (opa, the
one that cares, merely branches on pctx.Direction), so a misplaced
plugin is a probable misconfiguration rather than a guaranteed one, and
failing the boot would break configs that work today. A mismatch logs a
startup WARN (plugins.WarnPluginDirections, called beside the existing
WarnEmptyPipelines) and shows an advisory in abctl. Nil Directions means
unconstrained, so out-of-tree plugins are unaffected.

The wire field is plural and string-typed on purpose. `direction`
(singular) already means "the chain this configured instance sits in" and
stays untouched; `directions` is the type-level set of chains a plugin
supports. Strings rather than []Direction because Direction.UnmarshalJSON
decodes any unknown value to Inbound without erroring, which on a slice
would turn a future third value into a false "inbound" claim.

ConfigSchema:

mcp-parser, opa, session-budget and litellm-budget-track were
Configurable but not SchemaProvider, so /v1/plugins reported no field
metadata and abctl rendered them as bare names. All four now implement
it; mcp-parser and opa also gained the field annotations (defaults and
required flags taken from their applyDefaults/Configure, not guessed).

a2a-parser and inference-parser are deliberately excluded -- they have no
config at all, and pipeline/schema.go names them as legitimate omissions.

Also adds a "number" schema type for floats. litellm-budget-track is the
first plugin to expose float config; without it max_budget and the five
per-token rates would publish as "unknown" and render as quoted empty
strings in templates.

abctl:

Templates carry a `# chain:` line per plugin, and the pre-apply validator
flags a plugin pasted into a chain it doesn't declare. ValidationError
gains a Severity so advisories render under their own banner -- folding
them into the existing one would make its "framework reload will reject"
claim false.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Ed Snible <snible@us.ibm.com>
@coderabbitai

coderabbitai Bot commented Sep 2, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Team

Run ID: 5638fc0c-1859-4101-992f-0c6401e916a9

📥 Commits

Reviewing files that changed from the base of the PR and between 4ce9576 and ce9aff2.

📒 Files selected for processing (39)
  • authbridge/CLAUDE.md
  • authbridge/authlib/pipeline/directions_test.go
  • authbridge/authlib/pipeline/plugin.go
  • authbridge/authlib/pipeline/schema.go
  • authbridge/authlib/pipeline/schema_test.go
  • authbridge/authlib/plugins/a2aparser/plugin.go
  • authbridge/authlib/plugins/configschema_test.go
  • authbridge/authlib/plugins/contextguru/plugin.go
  • authbridge/authlib/plugins/cpex/plugin.go
  • authbridge/authlib/plugins/directions_test.go
  • authbridge/authlib/plugins/ibac/plugin.go
  • authbridge/authlib/plugins/inferenceparser/plugin.go
  • authbridge/authlib/plugins/jwtvalidation/plugin.go
  • authbridge/authlib/plugins/litellm_budgettrack/plugin.go
  • authbridge/authlib/plugins/mcpparser/plugin.go
  • authbridge/authlib/plugins/opa/plugin.go
  • authbridge/authlib/plugins/registry.go
  • authbridge/authlib/plugins/sessionbudget/plugin.go
  • authbridge/authlib/plugins/sparc/plugin.go
  • authbridge/authlib/plugins/staticinject/plugin.go
  • authbridge/authlib/plugins/tokenbroker/plugin.go
  • authbridge/authlib/plugins/tokenexchange/plugin.go
  • authbridge/authlib/plugins/warn.go
  • authbridge/authlib/sessionapi/catalog_adapter.go
  • authbridge/authlib/sessionapi/server.go
  • authbridge/cmd/abctl/README.md
  • authbridge/cmd/abctl/apiclient/client.go
  • authbridge/cmd/abctl/apiclient/client_test.go
  • authbridge/cmd/abctl/edit/templates.go
  • authbridge/cmd/abctl/edit/templates_test.go
  • authbridge/cmd/abctl/edit/validate.go
  • authbridge/cmd/abctl/edit/validate_test.go
  • authbridge/cmd/abctl/tui/catalog_pane.go
  • authbridge/cmd/abctl/tui/edit_overlay.go
  • authbridge/cmd/authbridge-cpex/main.go
  • authbridge/cmd/authbridge-envoy/main.go
  • authbridge/cmd/authbridge-proxy/catalog_test.go
  • authbridge/cmd/authbridge-proxy/main.go
  • authbridge/docs/plugin-catalog.md

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

Plugins now declare supported pipeline directions and configuration schemas. Session APIs and abctl expose this metadata. Runtime builds log placement warnings, while abctl reports direction mismatches as advisories.

Changes

Pipeline capability and schema contracts

Layer / File(s) Summary
Capability contracts and plugin declarations
authbridge/authlib/pipeline/*, authbridge/authlib/plugins/*
Plugin capabilities normalize and evaluate directions. Floating-point schema fields use number. Registered plugins declare directions, and configurable plugins expose schemas.
Catalog and pipeline metadata wiring
authbridge/authlib/plugins/registry.go, authbridge/authlib/sessionapi/*, authbridge/cmd/abctl/apiclient/*, authbridge/cmd/abctl/tui/catalog_pane.go
Catalog cloning preserves direction isolation. APIs and clients exchange optional directions metadata.
Runtime direction warnings
authbridge/authlib/plugins/warn.go, authbridge/cmd/authbridge-*/main.go, authbridge/cmd/authbridge-proxy/*
Pipeline builds warn when a configured plugin does not declare support for its chain. The warning is advisory and does not block building.
abctl templates and validation advisories
authbridge/cmd/abctl/edit/*, authbridge/cmd/abctl/tui/edit_overlay.go, authbridge/cmd/abctl/README.md, authbridge/docs/plugin-catalog.md, authbridge/CLAUDE.md
Templates show declared chains and numeric placeholders. Validation separates errors from direction warnings, and the TUI renders separate banners and prompts.

Estimated code review effort: 4 (Complex) | ~45 minutes

Merge Risk: ⚪ Minimal · up to ce9af

The PR adds machine-readable plugin directions and configuration schemas while preserving existing execution behavior; no actionable merge-blocking risk remains beyond normal checks and review.

Suggested reviewers: ibrahim2595, huang195

Sequence Diagram(s)

sequenceDiagram
  participant abctl
  participant SessionAPI
  participant PluginCatalog
  participant PipelineValidator
  participant PipelineBuild
  abctl->>SessionAPI: request plugin and pipeline metadata
  SessionAPI->>PluginCatalog: read normalized directions and schemas
  SessionAPI-->>abctl: return directions and field schemas
  abctl->>PipelineValidator: validate configured chain
  PipelineValidator-->>abctl: return errors and direction advisories
  PipelineBuild->>PluginCatalog: read plugin capabilities
  PipelineBuild-->>PipelineBuild: log mismatches without blocking build
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the two primary changes: declaring plugin pipeline directions and completing ConfigSchema coverage.
Docstring Coverage ✅ Passed Docstring coverage is 81.13% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 53 functions across 36 files. (3 skipped: 3…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 81.13% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 53 functions across 36 files. (3 skipped: 3 unsupported.)

✨ Finishing Touches 💡 1
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch plugin-directions-and-configschema
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

Some tools did not complete. Review the errors below.

🔧 golangci-lint (2.13.2)

level=error msg="[linters_context] typechecking error: build constraints exclude all Go files in /authbridge/cmd/authbridge-cpex"


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@huang195 huang195 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The design here is careful and the parts most likely to be wrong hold up — I checked them rather than taking the body's word for it. The block is not about the feature; it is that the branch cannot compile against main, and the conflict sits in exactly the two functions this PR modifies, so the rebase needs a deliberate resolution rather than a mechanical one.

State: 1 commit ahead, 34 behind, mergeable: false, rebaseable: false. And CI never ran on this SHA — only DCO and add-to-project executed (plus CodeRabbit). There is no Go build or test job in the check list, so none of the new tests have been run by CI, and the body's "go build, go vet, go test clean across all five modules" is a local claim only. That is a consequence of the conflict and should clear itself on rebase, but it means the three must-fixes below were all found by reading rather than by a red build.

Verified rather than assumed:

  • litellm-budget-track: Inbound is correct, which surprised me — it parses an LLM response, and inference-parser next to it is Outbound. docs/litellm-budgettrack-plugin.md settles it: the design doc says "inbound pipeline" throughout and its example config is pipeline:\n inbound:, because AuthBridge fronts the LiteLLM gateway there rather than sitting beside the caller. Worth a half-sentence in the catalog row, since the pairing reads like a mistake to anyone who does not know that topology.
  • All 14 declarations match docs/plugin-catalog.md's Direction column exactly, including opaBoth{Inbound, Outbound}.
  • Every ConfigSchema annotation matches the code. I checked each annotated field against the real applyDefaults/validate: opa's bundle_url required is genuinely enforced (validate() errors, called from Configure), agent_id_file's default really is /shared/client-id.txt, polling_min_delay/polling_max_delay really are 10/120, and mcp-parser's paths default really is ["/mcp"]. The field counts the tests assert (1 / 6 / 6) are right, and all four delegate to pipeline.SchemaOf, so drift is structurally impossible. The "taken from applyDefaults/Configure rather than guessed" claim is accurate.
  • The WARN is wired into all three binaries that build pipelines (-proxy, -envoy, -cpex), each placed beside WarnEmptyPipelines and before Build, as its godoc specifies. authbridge-praxis is correctly excluded — it never calls plugins.Build and does not call WarnEmptyPipelines either.
  • Both endpoints normalize. describePipeline uses pl.Capabilities().Normalize() and PluginsCatalog likewise, so /v1/pipeline and /v1/plugins cannot disagree on ordering — which is the thing canonicalDirections exists to guarantee.
  • Severity's zero value is safe. SeverityError = iota means all six pre-existing ValidationError sites keep blocking semantics; only the new direction check sets SeverityWarning. The banner split is complete and the y/N prompt correctly stays "apply this change?" when only advisories are present.
  • The pipeline tests are the good kind. TestNormalizeDoesNotMutateInput guards a real aliasing hazard, and the t.Skip guards in plugins/ do not actually fire — plugins_test.go blank-imports seven real plugins into the same test binary, so TestRegisteredPluginsDeclareDirections and TestCatalogCloneIsolatesDirections run against real data. The mutation-testing story in the body checks out: the clone test now t.Fatals instead of skipping.

Suggestions below are accuracy and coverage, not design. Nothing there blocks.

Author: esnible (MEMBER — maintainer)
Areas reviewed: Go (pipeline, plugins, sessionapi, abctl), tests, docs, security. 1 commit, signed off, no Co-Authored-By. CI: only DCO + add-to-project ran; no build/test job. No .claude/ or .vscode/ changes.

Assisted-By: Claude Code

if c.WritesBody {
c.ReadsBody = true
}
c.Directions = canonicalDirections(c.Directions)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

must-fix — this function will not compile against main. WritesBody no longer exists there: 6ca3fbb ("Rename WritesBody to WritesRequestBody") and 6440927 ("Split body-write capability by direction") both landed 2026-09-02, the day this PR was opened, and grep -c WritesBody on main's plugin.go is now 0.

main's version reads:

func (c PluginCapabilities) Normalize() PluginCapabilities {
	if c.WritesRequestBody || c.WritesResponseBody {
		c.ReadsBody = true
	}
	return c
}

So the resolution is to keep main's condition and add your one line to it:

	if c.WritesRequestBody || c.WritesResponseBody {
		c.ReadsBody = true
	}
	c.Directions = canonicalDirections(c.Directions)
	return c

The Directions field itself, canonicalDirections, and Supports all apply cleanly — the collision is only with the WritesBody line and the doc comment above it (which also names WritesBody, at :96). Flagging it explicitly because a rebase that resolves this hunk by taking your side compiles nowhere, and one that takes main's side silently drops the canonicalDirections call — losing the de-duplication and sort that the rest of the change depends on.

Description: caps.Description,
Requires: append([]string(nil), caps.Requires...),
RequiresAny: append([]string(nil), caps.RequiresAny...),
Directions: append([]pipeline.Direction(nil), caps.Directions...),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

must-fix — this is the more dangerous half of the conflict, because the naive resolution compiles and is wrong.

main has already replaced this hand-enumerated form with a struct copy, for precisely the reason your TestCatalogCloneIsolatesDirections docstring gives:

		// Struct copy, then reallocate the slices. Copying field-by-field
		// silently drops any capability added later; this picks them up.
		caps := in[i].Capabilities
		caps.Requires = append([]string(nil), in[i].Capabilities.Requires...)
		caps.RequiresAny = append([]string(nil), in[i].Capabilities.RequiresAny...)

Taking this PR's side of that conflict reverts that fix and drops two fields that now exist: WritesRequestBody and WritesResponseBody would be absent from the literal, so every Catalog() clone — and therefore every /v1/plugins response — would report them as false. tool-prune sets WritesRequestBody: true, so this would be observable immediately, and silently: no test on main covers those two fields surviving a clone.

Correct resolution — keep main's struct copy, add one line:

		caps.Directions = append([]pipeline.Direction(nil), in[i].Capabilities.Directions...)

Worth noting your clone test stays valuable after that change rather than becoming redundant: a struct copy duplicates the slice header but shares the backing array, so the explicit realloc is still what makes the mutation in TestCatalogCloneIsolatesDirections fail to reach the cache. The test guards the line above, not the struct copy.

t.Fatal("catalog is empty; plugin registration is broken")
}
for _, e := range cat {
if len(e.Directions) == 0 {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

must-fix — this test will fail after the rebase, on a plugin that does not exist on your branch yet.

tool-prune landed on main after the branch point. It is default-on (cmd/authbridge-proxy/plugins_toolprune.go is //go:build !exclude_plugin_toolprune), so it is in PluginsCatalog() for the default build, and it declares no Directions — meaning this loop hits t.Errorf("plugin %q publishes no directions"). TestRegisteredPluginsDeclareDirections in authlib/plugins would fail on it too if it were linked there.

That is your test working as designed, and it is the right failure to have. The fix is one line in toolprune/plugin.go's Capabilities():

		Directions: []pipeline.Direction{pipeline.Outbound},

Outbound is well-supported: docs/plugin-catalog.md on main already documents the row as Outbound, the package doc says "outbound inference requests", and its RequiresAny: []string{"inference-parser"} points at an outbound-only plugin, so any other value would make the dependency unsatisfiable in practice.

That is the only new plugin to account for — I diffed the catalog tables and main has 15 rows to this branch's 14, differing by tool-prune alone.

callee); "both" means the plugin evaluates on both pipelines. This column
is **declared in code** as `Capabilities().Directions` (see
`authlib/pipeline/plugin.go`), published on `/v1/plugins` as
`directions`, and asserted by a test — so the table below has a source of

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

suggestion — "asserted by a test — so the table below has a source of truth rather than drifting" claims more than the tests deliver. No Go test reads this file; the only reference to it anywhere in the test code is inside an error message string.

What the new tests actually assert is that every registered plugin declares something non-empty (TestRegisteredPluginsDeclareDirections, TestShippedCatalogPublishesDirections) and that the published values are "inbound"/"outbound". Neither compares against this column. So flipping a plugin's Directions to the opposite chain leaves every test green while this table says the reverse — which is the exact drift the sentence promises is now impossible.

Two honest options: parse the table in a test and compare (it is a stable pipe-delimited grid, and Both{Inbound, Outbound} is the only mapping needed), or drop "and asserted by a test" and "rather than drifting" and keep the accurate half — declared in code, published on /v1/plugins. The first is what makes the Direction column genuinely authoritative; the second at least stops over-promising.

}
}

// litellm-budget-track is the first plugin to expose float config, which

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

suggestion — "litellm-budget-track is the first plugin to expose float config" is not right, and the same claim is in the PR body ("litellm-budget-track is the first plugin with float config").

sparc got there first: sparc/plugin.go:94 is DenyScoreThreshold float64, and sparc has implemented ConfigSchema() since well before this PR (:220, untouched here). So sparc has been publishing a float field all along — as "unknown", via kindOf's default branch.

The consequence is worth a line in the PR description rather than a code change: this PR silently changes an existing plugin's published schema, flipping deny_score_threshold from "unknown" to "number" on /v1/plugins. That is the improvement working as intended, and I confirmed it is harmless downstream — placeholderFor is the only consumer that switches on Type, this PR teaches it "number", and deny_score_threshold's default:"0" short-circuits the placeholder path anyway. But it is an unremarked behaviour change to a plugin the PR never mentions, and no test pins it. A sparc case in TestSchemaOf_Floats' orbit — or just one sentence in the description — would cover it.

}{
"mcp-parser": {1, nil},
"opa": {6, []string{"bundle_url"}},
"session-budget": {18, []string{"redis_url"}},

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

suggestion — this expectation can never be exercised, and the comment in catalog_test.go defers to it, so session-budget's new ConfigSchema() ends up with zero coverage between them.

session-budget is registered only from cmd/authbridge-{proxy,envoy}/plugins_sessionbudget.go, under //go:build include_plugin_sessionbudget. No build of the authlib/plugins test binary reaches that registration, so Catalog() here never contains session-budget and this row is simply never matched — the test counts what it finds and passes. Meanwhile catalog_test.go:50-53 excuses its own omission with "covered by authlib/plugins' TestConfigSchemaShapes when linked." Each points at the other; neither runs it.

Cheapest fix is to test ConfigSchema() directly rather than through the registry — pipeline.SchemaOf(sessionBudgetConfig{}) from inside the sessionbudget package, where the build tag is not in the way. Failing that, drop the row and say plainly that it is uncovered, so the next reader does not trust a guarantee that is not there.

Related, :55: the docstring says these plugins "decode with DisallowUnknownFields", which is true for opa and mcp-parser but not for session-budget or litellm-budget-track — both use plain json.Unmarshal. For those two, TestConfigSchemaFieldNamesDecode would still pass with a wrong json name in the schema, since nothing rejects the unknown key. Worth narrowing the claim to the two plugins it holds for.

// and pick value placeholders. One of:
// "string", "int", "bool", "[]string", "object", "unknown".
// "string", "int", "number", "bool", "[]string", "object", "unknown".
// "number" is a float (per-token costs, budgets); "int" stays

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

suggestion — the stated rationale is contradicted by the only consumer, inside this same PR. "int" is said to stay separate "so a template can emit \"0\" vs \"0.0\" appropriately", but templates.go's placeholderFor returns "0" for both, and its new case "number" explains why that is the right call: "0 is valid YAML for a float, and avoids implying a fixed precision."

I agree with the code, not the comment — the two case bodies are now byte-identical, and no template emits 0.0. Keeping "number" distinct is still worth doing, just for a different reason: it is honest type metadata for consumers that are not this template renderer (a JSON-Schema emitter, a form generator, a validator that needs to know a value may be fractional). Restating it that way would leave the comment true, and would stop the next person collapsing the two cases on the grounds that the documented distinction is unused.

if e.Name == name {
p := apiclient.PipelinePlugin{
Name: e.Name,
Directions: e.Directions,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

suggestion — this is currently a dead write, and the comment above promises otherwise ("it is exactly the metadata a catalog browser wants").

showPluginDetail's renderer reads only the singular field — plugin_detail_pane.go:30-31 is if p.Direction != "" { ... "Direction:" ... } — and nothing anywhere reads p.Directions. So the value is copied onto the synthetic PipelinePlugin and then never displayed, which means the catalog pane still cannot tell an operator which chain a plugin belongs in, even though the data is now sitting right there.

Given the feature's whole premise is making placement visible, rendering it is the better resolution — a Chains: line beside Direction:, elided when empty, mirrors how the existing field degrades. Two lines in the detail pane. Otherwise drop the copy and the sentence, so the next reader is not misled into thinking the pane already surfaces it.

}
if len(vWarns) > 0 {
b.WriteString(styleWarn.Render(fmt.Sprintf(
"%d advisory — reload will accept, but check:",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit — missing plural(), so two advisories render as "2 advisory". The error branch four lines up gets this right ("%d validation issue%s" with plural(len(vErrs))), and the helper is already imported in this file, so it is "%d advisor%s" / plural — or "%d advisories"-style wording if you prefer, since plural returns "s" and "advisorys" is not a word.

README.md:165 shows the singular case, which is why it reads fine there.

While in this function: splitBySeverity and writeValidationLines have no test coverage at all — tui/edit_overlay_test.go is not in this PR and has never had a validationErrs case. The severity classification is well covered in edit/validate_test.go (SeveritiesCoexist is a good test), but the rendering split — the part that keeps the "reload will reject" banner truthful — is untested, and a mixed error+advisory set is exactly the case worth pinning.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: New/ToDo

Development

Successfully merging this pull request may close these issues.

3 participants