Skip to content

feat(RHIDP-17326): add pure OCI and npx skill metadata normalizers - #5072

Open
fullsend-ai-coder[bot] wants to merge 4 commits into
mainfrom
agent/RHIDP-17326-skill-metadata-normalizers
Open

fullsend-ai-coder[bot] wants to merge 4 commits into
mainfrom
agent/RHIDP-17326-skill-metadata-normalizers

Conversation

@fullsend-ai-coder

Copy link
Copy Markdown
Contributor

Summary

Implements OpenSpec task 1.2: pure OCI and npx metadata normalizers for the ai-skills-common shared library.

  • normalizeOciMetadata() — accepts parsed OCI SkillCard metadata and Markdown frontmatter with trusted source identity/integrity fields; returns schema-valid OciSkillRecord or diagnostics following D3 precedence (metadata.display-name > metadata.name > frontmatter name; metadata.version > frontmatter metadata.version > frontmatter version)
  • normalizeNpxMetadata() — accepts parsed npx discovery entry and frontmatter with trusted inputs; returns schema-valid NpxSkillRecord or diagnostics following D3 precedence (frontmatter name > entry name; frontmatter metadata.version > frontmatter version)
  • Native metadata fixtures — OCI SkillCard/Markdown and npx index/frontmatter fixtures with conflicting-version scenarios, full metadata, and minimal inputs for connector test reuse
  • Tag normalization — trim, lowercase, deduplicate, and validate tags against Backstage catalog tag rules; omit invalid/overlength tags with diagnostics
  • Author normalization — normalize author strings to [{ name: value }]; accept SkillCard authors with non-empty name and optional email; omit invalid entries with diagnostics
  • Extension allowlisting — preserve only extensions.oci.namespace, extensions.oci.prompt, and extensions.npx.type; SkillCard namespace stays in extensions, never overrides catalog namespace
  • Version precedence — invalid first-choice declared version retained for D5 fallback instead of silently selecting a lower-priority value

✔️ Checklist

  • A changeset describing the change and affected packages
  • Tests for new functionality (normalizer.test.ts with 47 test cases using the shared snapshot validator)
  • API report updated

Related to https://redhat.atlassian.net/browse/RHIDP-17326

Post-script verification

  • Branch is not main/master (agent/RHIDP-17326-skill-metadata-normalizers)
  • Secret scan passed (gitleaks — dc2f19a4fdb1f491c1e3696dfe9b59112510e99e..HEAD)
  • PR body secret scan passed (gitleaks — no-git)

Implement design D3 native-field mappings and precedence for both
OCI and npx sources in the ai-skills-common shared library. The
normalizers accept parsed, verified inputs and return schema-valid
SkillRecords or explicit per-record diagnostics.

OCI precedence: metadata.display-name > metadata.name > frontmatter
name for display name; metadata.version > frontmatter metadata.version
> frontmatter version for declared version. npx precedence: frontmatter
name > discovery entry name; frontmatter metadata.version > frontmatter
version.

Key behaviors:
- Trim scalar strings; normalize author strings to author objects
- Omit invalid optional values with diagnostics without failing records
- Preserve only allowlisted extensions (oci.namespace, oci.prompt,
  npx.type)
- Retain invalid first-choice version for D5 catalog fallback
- Authors never imply catalog ownership; SkillCard namespace stays in
  extensions
- Tags are trimmed, lowercased, deduplicated, and validated against
  Backstage catalog tag rules

Adds OCI SkillCard/Markdown and npx index/frontmatter fixtures with
conflicting-version scenarios. Tests validate all records pass the
shared snapshot validator.

Related to RHIDP-17326

Assisted-by: claude-opus-4-6
@rhdh-gh-app

rhdh-gh-app Bot commented Oct 1, 2026

Copy link
Copy Markdown

Important

This PR includes changes that affect public-facing API. Please ensure you are adding/updating documentation for new features or behavior.

Changed Packages

Package Name Package Path Changeset Bump Current Version
@red-hat-developer-hub/backstage-plugin-ai-skills-common workspaces/ai-integrations/plugins/ai-skills-common minor v0.1.0

@fullsend-ai-review

fullsend-ai-review Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 10:40 PM UTC · Completed 11:01 PM UTC

Commit: ab85de6 · View workflow run →

Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high · Cost: $6.81

@codecov

codecov Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.42857% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 64.10%. Comparing base (d69098f) to head (e2007e2).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #5072      +/-   ##
==========================================
+ Coverage   64.05%   64.10%   +0.05%     
==========================================
  Files        2717     2718       +1     
  Lines      107661   107857     +196     
  Branches    30323    30394      +71     
==========================================
+ Hits        68959    69147     +188     
- Misses      36858    36878      +20     
+ Partials     1844     1832      -12     
Flag Coverage Δ *Carryforward flag
adoption-insights 84.77% <ø> (ø) Carriedforward from 6cef97c
ai-integrations 87.73% <96.42%> (+0.48%) ⬆️
app-defaults 68.90% <ø> (ø) Carriedforward from 6cef97c
augment 46.67% <ø> (ø) Carriedforward from 6cef97c
boost 93.37% <ø> (ø) Carriedforward from 6cef97c
bulk-import 73.12% <ø> (ø) Carriedforward from 6cef97c
cost-management 13.56% <ø> (ø) Carriedforward from 6cef97c
dcm 74.40% <ø> (ø) Carriedforward from 6cef97c
e2e-adoption-insights 60.00% <ø> (ø) Carriedforward from 6cef97c
e2e-extensions 62.31% <ø> (ø) Carriedforward from 6cef97c
e2e-global-header 52.40% <ø> (ø) Carriedforward from 6cef97c
e2e-homepage 61.11% <ø> (ø) Carriedforward from 6cef97c
e2e-intelligent-assistant 45.49% <ø> (ø) Carriedforward from 6cef97c
e2e-orchestrator 49.49% <ø> (ø) Carriedforward from 6cef97c
e2e-orchestrator-plugin 49.48% <ø> (ø) Carriedforward from 6cef97c
e2e-quickstart 54.83% <ø> (ø) Carriedforward from 6cef97c
e2e-scorecard 49.77% <ø> (ø) Carriedforward from 6cef97c
e2e-theme 16.43% <ø> (ø) Carriedforward from 6cef97c
extensions 58.30% <ø> (ø) Carriedforward from 6cef97c
global-floating-action-button 71.18% <ø> (ø) Carriedforward from 6cef97c
global-header 69.10% <ø> (ø) Carriedforward from 6cef97c
homepage 55.05% <ø> (-0.11%) ⬇️ Carriedforward from 6cef97c
install-dynamic-plugins 84.02% <ø> (ø) Carriedforward from 6cef97c
intelligent-assistant 78.54% <ø> (ø) Carriedforward from 6cef97c
konflux 91.98% <ø> (ø) Carriedforward from 6cef97c
lightspeed 69.02% <ø> (ø) Carriedforward from 6cef97c
mcp-integrations 84.46% <ø> (ø) Carriedforward from 6cef97c
orchestrator 77.69% <ø> (ø) Carriedforward from 6cef97c
quickstart 65.83% <ø> (ø) Carriedforward from 6cef97c
sandbox 79.56% <ø> (ø) Carriedforward from 6cef97c
scorecard 89.02% <ø> (ø) Carriedforward from 6cef97c
theme 87.44% <ø> (ø) Carriedforward from 6cef97c
translations 7.91% <ø> (ø) Carriedforward from 6cef97c
x2a 78.48% <ø> (ø) Carriedforward from 6cef97c

*This pull request uses carry forward flags. Click here to find out more.


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update d69098f...e2007e2. Read the comment docs.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@fullsend-ai-review fullsend-ai-review Bot added the risk/moderate PR risk: moderate label Oct 1, 2026
@fullsend-ai-review

fullsend-ai-review Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Risk Assessment: moderate (2/5)

Details

Large line count (2207 lines) in a brand-new package is the primary risk driver, offset by bot authorship, no security or dependency concerns, no CI changes, no protected paths, and substantial test coverage (860 test lines for 803 implementation lines), yielding a moderate overall risk.

Previous run

Risk Assessment: moderate (2/5)

Details

Score of 2 (moderate) preserved from prior assessment: large additive PR (2086 lines, large blast radius) composed entirely of new files in a new shared library, bot-authored, with no protected paths, no CI or dependency changes, test ratio of 0.11, clean Tier 2 (all new files = 2), Tier 3 unavailable (external Jira); weights redistributed 62/38 yield composite 1.845, rounding to 2.

Previous run (2)

Risk Assessment: moderate (2/5)

Details

Score of 2 (moderate) reflects a large additive PR (1862 lines, large blast radius) that is entirely new files in a new shared library, bot-authored, with no protected paths, no CI or dependency changes, clean single-author git history, and strong per-LOC test coverage; Tier 3 unavailable (external Jira), weights redistributed 62/38, composite 1.54 rounds to 2.

@fullsend-ai-review

fullsend-ai-review Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Review

Findings

Medium

  • [Inconsistent validation logic] workspaces/ai-integrations/plugins/ai-skills-common/src/normalizer.ts:273 — CATALOG_TAG_PATTERN (/^[a-z0-9:+#]([a-z0-9:+#._-]*[a-z0-9:+#])?$/) allows dots and underscores in tags, while CATALOG_TAG_RE in catalog-helpers.ts (/^[a-z0-9:+#]+(-[a-z0-9:+#]+)*$/) does not. Both exist in the same package for tag validation but serve different code paths — the normalizer's pattern appears to more faithfully reproduce Backstage's actual tag pattern.
    Remediation: Align the two patterns if both are intended to validate against the same Backstage tag rules. If they intentionally serve different purposes, document why they differ.

Low

  • [Exported constant stability] workspaces/ai-integrations/plugins/ai-skills-common/src/normalizer.ts:273 — CATALOG_TAG_PATTERN is a hand-copied snapshot of Backstage's tag validation regex, not imported from @backstage/catalog-model. The existing JSDoc at lines 263–269 already documents provenance and semver implications.

  • [Code organization] workspaces/ai-integrations/plugins/ai-skills-common/src/normalizer.ts:280 — MAX_TAG_LENGTH = 63 is defined in both catalog-helpers.ts (private) and normalizer.ts (exported). The modules are deliberately uncoupled, but the two definitions could drift independently.

  • [Naming conventions] workspaces/ai-integrations/plugins/ai-skills-common/src/normalizer.ts:438 — The private function normalizeTags(candidates, diagnostics) shares its name with the public export normalizeTags(tags) from catalog-helpers.ts. The functions have different signatures and semantics.

  • [Edge case] workspaces/ai-integrations/plugins/ai-skills-common/src/normalizer.ts:474 — When a tag is whitespace-only, the diagnostic reports "does not match catalog tag pattern" rather than indicating the tag was empty after trimming. Functionally correct but misleading.


Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR
Previous run

Review

Findings

High

  • [stale-spec-audit] workspaces/ai-integrations/openspec/changes/oci-npx-skills-registry-demo/audit.md:3 — The PR modifies spec.md (new normalization scenarios) and tasks.md (task 1.2 marked done) but audit.md was not updated. AGENTS.md requires audit.md refresh when spec files change. The current timestamp (2026-10-01T00:00:00Z) predates this PR, and the audit narrative only mentions task 1.1 — task 1.2 completion is unacknowledged.
    Remediation: Update audit.md with a new timestamp and re-audit to cover the task 1.2 changes to spec.md and tasks.md.

Medium

  • [missing-diagnostic-consistency] workspaces/ai-integrations/plugins/ai-skills-common/src/normalizer.ts:607 — owner and lifecycle fields lack diagnoseNonStringCandidates calls in both normalizeOciMetadata (lines 607–608) and normalizeNpxMetadata (lines 720–721), unlike description, version, license, and compatibility which all call diagnoseNonStringCandidates. A non-string fm.metadata.owner or fm.metadata.lifecycle is silently dropped with no diagnostic. The README states "Non-string candidates at any priority level emit a diagnostic," and spec scenario "Diagnostic emission for incorrectly typed optional fields" requires diagnostics for any incorrectly typed optional scalar.
    Remediation: Add diagnoseNonStringCandidates('owner', [fm?.metadata?.owner], diagnostics) and diagnoseNonStringCandidates('lifecycle', [fm?.metadata?.lifecycle], diagnostics) after the firstNonEmptyString calls in both normalizer functions.

  • [hardcoded-tag-pattern-divergence] workspaces/ai-integrations/plugins/ai-skills-common/src/normalizer.ts:271 — CATALOG_TAG_PATTERN is a standalone hardcoded RegExp rather than imported from @backstage/catalog-model. If Backstage changes its tag validation pattern, this @public constant will silently diverge, and any future correction becomes a breaking change to downstream consumers.
    Remediation: Import the pattern from Backstage if exported, or add a CI check/snapshot test to detect drift. Document that the pattern is a snapshot of Backstage's rule at the time of authoring.

Low

  • [api-shape-patterns] workspaces/ai-integrations/plugins/ai-skills-common/src/normalizer.ts:104 — NpxFrontmatter (line 177) and OciMarkdownFrontmatter (line 104) are structurally identical (same top-level fields and nested metadata block, all typed unknown) with no shared ancestor. They represent distinct semantic domains that may legitimately diverge, but the current duplication creates maintenance burden if the shared shape ever changes.
    Remediation: Extract a shared base interface or alias one to the other with JSDoc explaining the semantic distinction.

  • [data-exposure] workspaces/ai-integrations/plugins/ai-skills-common/src/normalizer.ts:453 — Overlength tag values are embedded verbatim in diagnostic messages without truncation. Similarly, String(entry.type) at line 753 could produce arbitrarily long output. Risk is mitigated by the normalizer being a pure function receiving already-parsed input, but the pattern could be improved.
    Remediation: Truncate user-controlled values before interpolating them into diagnostic messages (e.g., slice to 80 chars).

  • [required-field-diagnostic-inconsistency] workspaces/ai-integrations/plugins/ai-skills-common/src/normalizer.ts:552 — For the required name field, only a terminal diagnostic is emitted when no valid name is found — non-string higher-priority candidates are silently skipped by firstNonEmptyString with no per-candidate warning. This is inconsistent with optional fields which emit per-candidate type-mismatch diagnostics, and the inconsistency is part of the now-@public behavioral contract.
    Remediation: Document the behavioral distinction explicitly in the JSDoc, or align name-candidate handling with optional scalar handling by calling diagnoseNonStringCandidates on the name candidates.

  • [public-fixture-constants] workspaces/ai-integrations/plugins/ai-skills-common/src/index.ts:87 — New fixture constants (ociSkillCard*, ociMarkdownFrontmatter*, npxDiscoveryEntry*, npxFrontmatter*, etc.) exported as @public. Changing these values in any future release is a breaking change for downstream consumers. Pattern is consistent with PR feat(RHIDP-17325): add skills-common shared contract library #5061 but each addition increases the maintenance surface.
    Remediation: Consider @internal or @alpha for fixtures intended purely for internal testing.


Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR
Previous run (2)

Review

Findings

Medium

  • [scope-authorization-documentation] workspaces/ai-integrations/openspec/changes/oci-npx-skills-registry-demo/tasks.md:6 — Task 1.2 checkbox remains unchecked after this PR fully implements it. AGENTS.md requires updating affected openspec documentation as part of the same commit. PR feat(RHIDP-17325): add skills-common shared contract library #5061 established the pattern: task 1.1 was marked [x] and a narrative note was added. This PR omits both steps for task 1.2.
    Remediation: Mark task 1.2 as [x] and add a trailing note similar to the task 1.1 note.

  • [scope-authorization-documentation] workspaces/ai-integrations/openspec/changes/oci-npx-skills-registry-demo/specs/skills-common/spec.md — The spec.md "Requirement: Explicit metadata normalization" section lacks scenarios for tag normalization, author string-to-object normalization, authors never implying catalog ownership, and diagnostic emission for incorrectly typed optional fields. The shared library now exports normalizer functions that the spec does not reference.
    Remediation: Add behavioral scenarios to spec.md covering normalizer function behavior: tag validation/dedup, author string shorthand, diagnostic emission, and the library exporting normalizer functions.

  • [Missing API documentation] workspaces/ai-integrations/plugins/ai-skills-common/README.md:17 — The README Types table and prose sections cover only the pre-existing SkillRecord/SkillSnapshot surface. The PR adds normalizeNpxMetadata, normalizeOciMetadata, and thirteen new public types that are not mentioned anywhere in the README. The API is documented in report.api.md and JSDoc, but the README overview is incomplete.
    Remediation: Add a "Normalization" section documenting the new functions and types.

Low

  • [missing diagnostic / inconsistency] workspaces/ai-integrations/plugins/ai-skills-common/src/normalizer.ts:391 — normalizeTags silently skips non-array candidates without emitting a diagnostic, unlike normalizeAuthors which reports unsupported types. A scalar string tag value (e.g., tags: 'single-tag' in YAML) is silently dropped.
    Remediation: Add a diagnostic when a non-array candidate is encountered in normalizeTags.

  • [test coverage gap] workspaces/ai-integrations/plugins/ai-skills-common/src/normalizer.test.ts — No test exercises the OCI tag fallback path from SkillCard metadata.tags to frontmatter metadata.tags.

  • [missing diagnostic for fallback] workspaces/ai-integrations/plugins/ai-skills-common/src/normalizer.ts:505 — When a higher-priority candidate for an optional scalar field has a non-string type but a lower-priority candidate provides a valid string, no diagnostic is emitted about the type mismatch at the higher-priority source.

  • [API shape patterns] workspaces/ai-integrations/plugins/ai-skills-common/src/normalizer.ts:583 — The local oci builder is typed as Record<string, string> rather than the concrete OciExtensions interface already defined in ./types.
    Remediation: Import OciExtensions and use it instead of Record<string, string>.

  • [Behavioral contract not captured in public types] workspaces/ai-integrations/plugins/ai-skills-common/src/normalizer.ts:238 — The public normalizer functions have behavioral contracts (tag max length 63, CATALOG_TAG_PATTERN, npx extension allowlist) not expressed in TypeScript types or report.api.md.
    Remediation: Export MAX_TAG_LENGTH and CATALOG_TAG_PATTERN as @public constants, or document them in JSDoc.

  • [Input/output type asymmetry] workspaces/ai-integrations/plugins/ai-skills-common/src/normalizer.ts:138 — NpxDiscoveryEntry.type is typed unknown but the normalizer silently drops any value other than 'skill-md'. Callers cannot discover the strict allowlist from the type signature.
    Remediation: Narrow NpxDiscoveryEntry.type to 'skill-md' | string or add @remarks JSDoc listing accepted values.

  • [Forward-compatibility trap] workspaces/ai-integrations/plugins/ai-skills-common/src/normalizer.ts:40 — All optional fields in the input interfaces are typed unknown. Future strengthening to concrete types would be a breaking TypeScript change.
    Remediation: Document in interface JSDoc that unknown field types are deliberate and stable for the v1 contract.

  • [Fixture values locked into versioned public API] workspaces/ai-integrations/plugins/ai-skills-common/src/fixtures.ts:245 — 17 fixture constants are exported as @public. Any field value change requires a semver minor bump, expanding the stable-value surface.


Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR

fullsend-ai-review[bot]

This comment was marked as outdated.

fullsend-ai-review[bot]

This comment was marked as outdated.

@johnmcollier

Copy link
Copy Markdown
Member

/fs-fix address and resolve review comments

@fullsend-ai-coder

fullsend-ai-coder Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor Author

🤖 Finished Fix · ✅ Success · Started 7:10 PM UTC · Completed 7:29 PM UTC

Commit: ab85de6 · View workflow run →

Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high · Cost: $4.86

- Mark task 1.2 as complete in tasks.md with narrative note
- Add spec.md scenarios for tag validation, author normalization, diagnostics
- Add README.md Normalization section documenting new functions and types
- Add diagnostic for non-array tag candidates in normalizeTags
- Emit diagnostics for non-string candidates at all priority levels
- Use OciExtensions type instead of Record<string, string>
- Export CATALOG_TAG_PATTERN and MAX_TAG_LENGTH as @public constants
- Document unknown field types as deliberate v1 contract stability choice
- Add @remarks to NpxDiscoveryEntry.type documenting accepted values
- Add tests for OCI tag fallback, non-array tag diagnostic, and non-string fallback diagnostic
- Update API report for new public exports

Addresses #5072

Assisted-by: claude-opus-4-6
@fullsend-ai-coder

Copy link
Copy Markdown
Contributor Author

🔧 Fix agent — iteration 1 (human-triggered)

Addressed all 11 review findings: 10 fixed (documentation gaps in tasks.md, spec.md, README.md; missing diagnostics for non-array tags and non-string fallback candidates; OciExtensions type usage; exported behavioral constants; JSDoc for unknown type stability and NpxDiscoveryEntry.type accepted values; 3 new tests) and 1 disagreement (fixture @public visibility is intentional). All 119 tests pass.

Fixed (10):

  1. Task 1.2 checkbox remains unchecked after this PR fully implements it (workspaces/ai-integrations/openspec/changes/oci-npx-skills-registry-demo/tasks.md): Marked task 1.2 as [x] and added a trailing narrative note documenting PR feat(RHIDP-17326): add pure OCI and npx skill metadata normalizers #5072 implementation, following the PR feat(RHIDP-17325): add skills-common shared contract library #5061 pattern for task 1.1
  2. spec.md lacks scenarios for tag normalization, author string-to-object normalization, authors never implying catalog ownership, diagnostic emission, and normalizer exports (workspaces/ai-integrations/openspec/changes/oci-npx-skills-registry-demo/specs/skills-common/spec.md): Added five behavioral scenarios: Tag validation and deduplication, Author string shorthand, Authors never imply catalog ownership, Diagnostic emission for incorrectly typed optional fields, and Normalizer function exports
  3. README Types table and prose sections cover only pre-existing surface; new normalizer functions and types are not mentioned (workspaces/ai-integrations/plugins/ai-skills-common/README.md): Added a Normalization section documenting normalizeOciMetadata, normalizeNpxMetadata, all input/result types, constants (MAX_TAG_LENGTH, CATALOG_TAG_PATTERN), and behavioral contract
  4. normalizeTags silently skips non-array candidates without emitting a diagnostic (workspaces/ai-integrations/plugins/ai-skills-common/src/normalizer.ts): Added diagnostic emission for non-array tag candidates with unsupported type message, matching normalizeAuthors pattern. Added corresponding test.
  5. No test exercises the OCI tag fallback path from SkillCard metadata.tags to frontmatter metadata.tags (workspaces/ai-integrations/plugins/ai-skills-common/src/normalizer.test.ts): Added test 'falls back from SkillCard metadata.tags to frontmatter metadata.tags' verifying the D3 tag precedence fallback path
  6. When a higher-priority candidate for an optional scalar field has a non-string type but a lower-priority candidate provides a valid string, no diagnostic is emitted about the type mismatch (workspaces/ai-integrations/plugins/ai-skills-common/src/normalizer.ts): Refactored diagnostic emission to use diagnoseNonStringCandidates which always checks all candidates for non-string types regardless of whether a valid string was found. Added corresponding test.
  7. The local oci builder is typed as Record<string, string> rather than the concrete OciExtensions interface (workspaces/ai-integrations/plugins/ai-skills-common/src/normalizer.ts): Imported OciExtensions from ./types and used it instead of Record<string, string> for the oci extension builder
  8. Behavioral contracts (tag max length 63, CATALOG_TAG_PATTERN) not expressed in TypeScript types or report.api.md (workspaces/ai-integrations/plugins/ai-skills-common/src/normalizer.ts): Exported CATALOG_TAG_PATTERN and MAX_TAG_LENGTH as @public constants, added them to index.ts exports, and updated API report
  9. NpxDiscoveryEntry.type is typed unknown but the normalizer silently drops any value other than skill-md (workspaces/ai-integrations/plugins/ai-skills-common/src/normalizer.ts): Added @remarks JSDoc to NpxDiscoveryEntry.type documenting that only 'skill-md' is accepted and other values are omitted with a diagnostic
  10. All optional fields in the input interfaces are typed unknown; future strengthening to concrete types would be a breaking TypeScript change (workspaces/ai-integrations/plugins/ai-skills-common/src/normalizer.ts): Added @remarks JSDoc to OciSkillCardMetadata, OciMarkdownFrontmatter, NpxDiscoveryEntry, and NpxFrontmatter documenting that unknown field types are deliberate and stable for the v1 contract

Disagreed (1):

  1. 17 fixture constants are exported as @public; any field value change requires a semver minor bump: Fixtures are intentionally @public to enable connector and provider contract tests to import them directly. This is the documented design: the package exports valid and invalid fixtures for reuse. Reducing visibility would break the intended public API contract. The semver cost is acceptable since fixture values are stable test constants.

Tests: passed

Decision points
  • Export CATALOG_TAG_PATTERN and MAX_TAG_LENGTH as @public vs document in JSDoc only (alternatives: Export as public constants, Document in JSDoc only; rationale: Exporting as public constants provides programmatic access to the behavioral contract, allowing consumers to validate tags before submission. JSDoc alone would leave the contract undiscoverable at the type level.)
  • Fixture @public visibility (alternatives: Reduce visibility to @internal, Keep as @public; rationale: Fixtures are designed for cross-package contract test reuse. Making them internal would defeat their purpose as shared test data.)

Next steps:

  • /fs-review — request a re-review of the changes
  • /fs-fix <your instruction> — run another fix pass with specific guidance
  • Push commits directly — review re-runs automatically on push
    Updated by fullsend fix agent

@johnmcollier

Copy link
Copy Markdown
Member

/fs-review

@fullsend-ai-review

fullsend-ai-review Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 7:31 PM UTC · Completed 7:54 PM UTC

Commit: beef011 · View workflow run →

Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high · Cost: $8.84

fullsend-ai-review[bot]

This comment was marked as outdated.

@johnmcollier

Copy link
Copy Markdown
Member

/fs-fix address review comments

@fullsend-ai-coder

fullsend-ai-coder Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor Author

🤖 Finished Fix · ❌ Failure (post-script /home/runner/work/rhdh-plugins/rhdh-plugins/.fullsend/.fullsend-cache/resources/sha256/f0b37e364cfe852baffb406950fba12d481781d1ec6d4ec0d4d22f581fa19dd8/scripts/post-fix.sh failed: exit sta…) · Started 8:00 PM UTC · Completed 8:18 PM UTC

Commit: beef011 · View workflow run →

Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high · Cost: $4.01

Resolve the task checklist and generated ai-skills-common API report conflicts.

Assisted-by: GPT-6
@fullsend-ai-coder

Copy link
Copy Markdown
Contributor Author

⚠️ Post-fix script failed — Push rejected (exit code 1)

The fix agent completed, but the post-fix script failed before finishing.

Workflow run: https://github.com/redhat-developer/rhdh-plugins/actions/runs/37057585913

Details:
To https://github.com/redhat-developer/rhdh-plugins.git
! [rejected] agent/RHIDP-17326-skill-metadata-normalizers -> agent/RHIDP-17326-skill-metadata-normalizers (fetch first)
error: failed to push some refs to 'https://github.com/redhat-developer/rhdh-plugins.git'
hint: Updates were rejected because the remote contains work that you do not
hint: have locally. This is usually caused by another repository pushing to
hint: the same ref. If you want to integrate the remote changes, use
hint: 'git pull' before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
To https://github.com/redhat-developer/rhdh-plugins.git
! [rejected] agent/RHIDP-17326-skill-metadata-normalizers -> agent/RHIDP-17326-skill-metadata-normalizers (stale info)
error: failed to push some refs to 'https://github.com/redhat-developer/rhdh-plugins.git'
Please check the workflow logs for full details and retry with /fs-fix if appropriate.

@johnmcollier

Copy link
Copy Markdown
Member

/fs-fix address review comments

@fullsend-ai-coder

fullsend-ai-coder Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor Author

🤖 Finished Fix · ✅ Success · Started 8:20 PM UTC · Completed 8:36 PM UTC

Commit: 6cef97c · View workflow run →

Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high · Cost: $4.76

- [HIGH] stale-spec-audit: Update audit.md to acknowledge task 1.2
  completion with a fresh timestamp
- [MEDIUM] missing-diagnostic-consistency: Add diagnoseNonStringCandidates
  calls for owner and lifecycle fields in both normalizeOciMetadata and
  normalizeNpxMetadata, with test coverage
- [MEDIUM] hardcoded-tag-pattern-divergence: Add JSDoc documenting that
  CATALOG_TAG_PATTERN is a snapshot of Backstage's rule and not imported
  from @backstage/catalog-model (not a dependency of this library)
- [LOW] api-shape-patterns: Extract shared MarkdownFrontmatter base
  interface with OciMarkdownFrontmatter and NpxFrontmatter as type aliases
- [LOW] data-exposure: Truncate user-controlled values to 80 chars in
  diagnostic messages (tag values, entry.type)
- [LOW] required-field-diagnostic-inconsistency: Document the intentional
  behavioral distinction in code comments (required name field uses
  terminal diagnostic vs per-candidate warnings for optional fields)

Addresses #5072

Assisted-by: claude-opus-4-6
@fullsend-ai-coder

Copy link
Copy Markdown
Contributor Author

🔧 Fix agent — iteration 2 (human-triggered)

Addressed all 7 review findings: 6 fixed (audit.md task 1.2 acknowledgment, owner/lifecycle diagnostic consistency with 4 new tests, CATALOG_TAG_PATTERN provenance documentation, shared MarkdownFrontmatter base interface, diagnostic value truncation, name field diagnostic documentation) and 1 disagreement (fixture @public visibility is intentional for cross-package test reuse). All 51 tests pass.

Fixed (6):

  1. stale-spec-audit: audit.md not updated to cover task 1.2 changes (workspaces/ai-integrations/openspec/changes/oci-npx-skills-registry-demo/audit.md): Updated audit.md timestamp and added task 1.2 acknowledgment describing normalizeOciMetadata and normalizeNpxMetadata implementation in PR feat(RHIDP-17326): add pure OCI and npx skill metadata normalizers #5072
  2. missing-diagnostic-consistency: owner and lifecycle fields lack diagnoseNonStringCandidates calls in both normalizeOciMetadata and normalizeNpxMetadata (workspaces/ai-integrations/plugins/ai-skills-common/src/normalizer.ts): Added diagnoseNonStringCandidates calls for owner and lifecycle in both normalizer functions, with 4 new tests covering non-string owner and lifecycle in OCI and npx normalizers
  3. hardcoded-tag-pattern-divergence: CATALOG_TAG_PATTERN is a standalone hardcoded RegExp rather than imported from @backstage/catalog-model (workspaces/ai-integrations/plugins/ai-skills-common/src/normalizer.ts): Added @remarks JSDoc documenting that CATALOG_TAG_PATTERN is a snapshot of Backstage's rule at the time of authoring. Cannot import from @backstage/catalog-model because it is not a dependency of this library; adding it solely for a regex pattern would be overreach
  4. api-shape-patterns: NpxFrontmatter and OciMarkdownFrontmatter are structurally identical with no shared ancestor (workspaces/ai-integrations/plugins/ai-skills-common/src/normalizer.ts): Extracted shared MarkdownFrontmatter base interface exported as @public. OciMarkdownFrontmatter and NpxFrontmatter are now type aliases with JSDoc explaining the semantic distinction and that they may diverge independently in future versions
  5. data-exposure: Overlength tag values and entry.type embedded verbatim in diagnostic messages without truncation (workspaces/ai-integrations/plugins/ai-skills-common/src/normalizer.ts): Added truncateForDiagnostic helper that slices values to 80 characters with an ellipsis suffix. Applied to overlength/invalid tag diagnostics and entry.type diagnostic
  6. required-field-diagnostic-inconsistency: name field only emits terminal diagnostic, no per-candidate type-mismatch warnings unlike optional fields (workspaces/ai-integrations/plugins/ai-skills-common/src/normalizer.ts): Documented the intentional behavioral distinction in code comments: name is a required field whose absence aborts normalization, so the terminal diagnostic is authoritative and per-candidate warnings would be noise on an already-failed result

Disagreed (1):

  1. public-fixture-constants: New fixture constants exported as @public, changing values requires semver minor bump: Fixtures are intentionally @public for cross-package connector and provider contract test reuse. This is the documented design pattern (consistent with PR feat(RHIDP-17325): add skills-common shared contract library #5061). Reducing visibility to @internal would defeat their purpose as shared test data. The semver cost is acceptable since fixture values are stable test constants.

Tests: passed

Decision points
  • Extract shared MarkdownFrontmatter base vs keep independent interfaces (alternatives: Extract shared base interface with type aliases, Keep independent identical interfaces; rationale: Extracting the shared base eliminates maintenance burden from duplicated shape while preserving the ability for OCI and npx domains to diverge via future interface extension. Type aliases maintain backward compatibility.)
  • Document CATALOG_TAG_PATTERN divergence risk vs import from @backstage/catalog-model (alternatives: Import pattern from @backstage/catalog-model, Add JSDoc documenting snapshot provenance; rationale: @backstage/catalog-model is not a dependency of this library. Adding a runtime dependency solely for a regex constant would couple the library to Backstage's release cycle. JSDoc documentation provides equivalent awareness for maintainers.)
  • Document name field diagnostic behavior vs align with optional field handling (alternatives: Add per-candidate diagnostics for name candidates, Document the behavioral distinction; rationale: Adding per-candidate diagnostics for name would add noise to an already-failed result (null record). The terminal diagnostic is authoritative for required fields. Documentation makes the design choice explicit.)

Next steps:

  • /fs-review — request a re-review of the changes
  • /fs-fix <your instruction> — run another fix pass with specific guidance
  • Push commits directly — review re-runs automatically on push
    Updated by fullsend fix agent

@sonarqubecloud

sonarqubecloud Bot commented Oct 2, 2026

Copy link
Copy Markdown

@johnmcollier

Copy link
Copy Markdown
Member

/fs-review

@fullsend-ai-review

fullsend-ai-review Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 8:44 PM UTC · Completed 9:05 PM UTC

Commit: e2007e2 · View workflow run →

Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high · Cost: $7.53

@fullsend-ai-review fullsend-ai-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

See the review comment for full details.

*
* @public
*/
export const CATALOG_TAG_PATTERN = /^[a-z0-9:+#]([a-z0-9:+#._-]*[a-z0-9:+#])?$/;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[medium] Inconsistent validation logic

CATALOG_TAG_PATTERN (/^a-z0-9:+#?$/) allows dots and underscores in tags, while CATALOG_TAG_RE in catalog-helpers.ts (/^[a-z0-9:+#]+(-[a-z0-9:+#]+)*$/) does not. Both exist in the same package for tag validation but serve different code paths. The normalizer pattern appears to more faithfully reproduce Backstage actual tag pattern.

Suggested fix: Align the two patterns if both are intended to validate against the same Backstage tag rules. If they intentionally serve different purposes, document why they differ.

*
* @public
*/
export const CATALOG_TAG_PATTERN = /^[a-z0-9:+#]([a-z0-9:+#._-]*[a-z0-9:+#])?$/;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] Exported constant stability

CATALOG_TAG_PATTERN is a hand-copied snapshot of Backstage tag validation regex, not imported from @backstage/catalog-model. The existing JSDoc at lines 263-269 already documents provenance and semver implications.

*
* @public
*/
export const MAX_TAG_LENGTH = 63;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] Code organization

MAX_TAG_LENGTH = 63 is defined in both catalog-helpers.ts (private) and normalizer.ts (exported). The modules are deliberately uncoupled, but the two definitions could drift independently.

* validated against Backstage's tag rules, and deduplicated.
* Invalid or overlength tags are omitted with diagnostics.
*/
function normalizeTags(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] Naming conventions

The private function normalizeTags(candidates, diagnostics) shares its name with the public export normalizeTags(tags) from catalog-helpers.ts. The functions have different signatures and semantics.

raw,
)}' omitted — exceeds ${MAX_TAG_LENGTH} characters`,
});
} else {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] Edge case

When a tag is whitespace-only, the diagnostic reports does not match catalog tag pattern rather than indicating the tag was empty after trimming. Functionally correct but misleading.

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

Labels

ready-for-review Triggers review agent dispatch risk/moderate PR risk: moderate workspace/ai-integrations

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant