Skip to content

fix: Sanitise marketplace name to kebab-case in output mappers#2008

Merged
danielmeppiel merged 6 commits into
mainfrom
sergio-sisternes-epam-fix-marketplace-name-sanitisation
Jul 3, 2026
Merged

fix: Sanitise marketplace name to kebab-case in output mappers#2008
danielmeppiel merged 6 commits into
mainfrom
sergio-sisternes-epam-fix-marketplace-name-sanitisation

Conversation

@sergio-sisternes-epam

Copy link
Copy Markdown
Collaborator

Description

Repository names containing dots, underscores, or other non-kebab-case characters (e.g. my.marketplace) flow into marketplace.json verbatim. The Copilot App rejects these with a validation error: "name Marketplace name must be kebab-case (letters, numbers, and hyphens)".

Since we cannot control how users name their repositories, this PR adds a sanitise_marketplace_name() function that converts names to kebab-case at the output boundary -- when composing marketplace.json. The function lowercases, replaces non-alphanumeric characters with hyphens, collapses consecutive hyphens, and strips leading/trailing hyphens.

Applied in both ClaudeMarketplaceMapper and CodexMarketplaceMapper. Internal resolution, registry lookups, and install flows remain untouched -- the original name is preserved everywhere except the consumer-facing JSON "name" field. The Codex displayName also keeps the original for human readability.

Type of change

  • Bug fix
  • New feature
  • Documentation
  • Maintenance / refactor

Testing

  • Tested locally
  • All existing tests pass (1409 marketplace unit tests)
  • Added tests for new functionality (tests/unit/marketplace/test_name_sanitisation.py -- 16 parametrised cases covering dots, underscores, spaces, slashes, special characters, mixed case, edge cases, and fallback behaviour)

Spec conformance (OpenAPM v0.1)

  • N/A -- this PR does not change OpenAPM-observable behaviour.

Repository names like 'my.marketplace' flow into marketplace.json
verbatim, causing the Copilot App to reject them with a kebab-case
validation error. Add sanitise_marketplace_name() that lowercases,
replaces non-alphanumeric characters with hyphens, collapses
consecutive hyphens, and strips leading/trailing hyphens. Applied
in both ClaudeMarketplaceMapper and CodexMarketplaceMapper.

Co-authored-by: Copilot <[email protected]>
Copilot AI review requested due to automatic review settings July 3, 2026 14:31
@sergio-sisternes-epam sergio-sisternes-epam changed the title Sanitise marketplace name to kebab-case in output mappers fix: Sanitise marketplace name to kebab-case in output mappers Jul 3, 2026
@sergio-sisternes-epam sergio-sisternes-epam added the panel-review Trigger the apm-review-panel gh-aw workflow label Jul 3, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds a marketplace-name normalization step at the output boundary so generated marketplace.json uses Copilot-App-compatible kebab-case names, without changing internal resolution or lookup behavior.

Changes:

  • Introduced a helper to convert config.name into kebab-case (lowercase, non-alphanumerics to -, collapse/trim).
  • Applied the sanitization when composing both Claude and Codex marketplace JSON outputs (Codex keeps the original name for interface.displayName).
  • Added a dedicated unit test suite covering common and edge-case inputs.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
src/apm_cli/marketplace/output_mappers.py Adds name-sanitization helper and uses it when emitting the top-level name field for Claude/Codex marketplace JSON.
tests/unit/marketplace/test_name_sanitisation.py Adds parameterized tests validating kebab-case conversion and fallback behavior.

Comment thread src/apm_cli/marketplace/output_mappers.py Outdated
Comment thread src/apm_cli/marketplace/output_mappers.py Outdated
Align with codebase convention (American spelling) and clarify
docstring to describe output-boundary normalization of the
marketplace identity, not a repo-name-only concern.

Co-authored-by: Copilot <[email protected]>
@github-actions github-actions Bot removed the panel-review Trigger the apm-review-panel gh-aw workflow label Jul 3, 2026
@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown

APM Review Panel: ship_with_followups

Sanitize marketplace name to kebab-case at output boundary -- correct fix, clean scope, but silent mutation warning and compose()-tier regression trap must be tracked as follow-ups before next release.

cc @sergio-sisternes-epam @danielmeppiel -- a fresh advisory pass is ready for your review.

The fix is correct, well-scoped, and removes a concrete adoption blocker: enterprise repos with dots, underscores, or mixed-case names in apm.yml were silently producing marketplace.json files rejected by Copilot App's kebab-case validation. The implementation is appropriately confined to the output boundary, leaving resolution, registry lookups, and install flows untouched. Both mappers are updated consistently, and the 16 unit tests confirm the function's correctness in isolation. This is a net-positive merge.

Five of seven active panelists independently raised the same issue: when sanitize_marketplace_name() rewrites config.name, neither mapper emits any user-visible signal. The MapperResult.warnings channel is already plumbed from both mappers through builder.py to CLI output -- the existing duplicate-name warning logic proves the path works end-to-end. The omission of a warning is an oversight, not a design decision, and the five-panelist convergence is the highest-signal finding in this panel. Separately, devx-ux-expert and supply-chain-security-expert both flag the all-punctuation fallback to the literal string "marketplace" as a silent fail-open that creates a registry collision risk between distinct marketplace configs. I side with their recommendation to raise a BuildError when sanitization yields an empty result, rather than cli-logging-expert's softer "louder warning" position -- a collision at the registry level is a defect, not a degraded UX, and build-time failure is the correct gate.

The test-coverage-expert finding carries the greatest structural weight by evidence-weighting rules: removing sanitize_marketplace_name() from either mapper's compose() leaves every existing test green, because all integration fixtures already use valid kebab-case names. Evidence outcome=missing on an integration-tier user promise ranks above all recommended-opinion findings in this panel. It is the first tracking issue to open post-merge. The two doc gaps -- a missing CHANGELOG entry and a now-factually-incorrect "only structural transform" claim in publish-to-a-marketplace.md -- are standard hygiene for any behavior-changing fix and must land in the same release cycle.

Dissent. cli-logging-expert recommends a louder warning for the empty-sanitization fallback; devx-ux-expert and supply-chain-security-expert recommend a BuildError. I side with BuildError: silent fallback to the generic name "marketplace" is a registry collision-risk defect, not a degraded-experience issue, and build-time failure is the appropriate gate.

Aligned with: Portable by manifest -- normalizing the name field ensures generated marketplace.json conforms to the registry spec regardless of how the author names their project in apm.yml; Pragmatic as npm -- npm, cargo, and brew all surface warnings when a config value is coerced at publish time, and silent mutation deviates from the "no surprises" contract; Secure by default -- the all-punctuation fallback is a fail-open that creates a registry collision surface, and replacing it with a BuildError hardens the default.

Growth signal. The launch beat for the next release note: "APM now works with any repo naming convention -- dots, underscores, enterprise prefixes -- no renaming required." Enterprise repos with dotted names (e.g. my.company.tools) were a hard no-go before this fix; they now just work. This should be the headline entry in the next CHANGELOG and release announcement to re-engage users who hit the Copilot App validation error and churned.

Panel summary

Persona B R N Takeaway
Python Architect 0 1 1 Clean output-boundary normalization; add a build diagnostic when config.name is mutated, and thread warnings through CodexMarketplaceMapper.
CLI Logging Expert 0 2 1 Silent name mutation with no user feedback; correct emission channel (MapperResult.warnings) exists and is already used by duplicate-name logic.
DevX UX Expert 0 2 1 Silent name mutation and opaque fallback undermine the "output matches config" mental model; the warning channel exists but goes unused.
Supply Chain Security Expert 0 2 1 Sanitization is output-only and does not touch auth/resolution flows; two recommended fixes for silent fail-open and lossy transform without warning.
OSS Growth Hacker 0 2 1 High-value enterprise friction removal; add CHANGELOG entry and a CLI warning to surface the silent name transformation.
Doc Writer 0 2 1 CHANGELOG.md [Unreleased] missing fix entry; publish-to-a-marketplace.md "only structural transform" claim is now incorrect; Pitfalls section silent on name normalization.
Test Coverage Expert 0 1 1 16 unit tests prove sanitize_marketplace_name() correct in isolation; no integration-tier regression trap for the call-site within compose() -- removing the call from either mapper would silently pass all tests.

B = blocking-severity findings, R = recommended, N = nits.
Counts are signal strength, not gates. The maintainer ships.

Top 5 follow-ups

  1. [Test Coverage Expert] Add compose()-tier regression tests that construct a MarketplaceConfig with name='my.marketplace', call ClaudeMarketplaceMapper.compose() and CodexMarketplaceMapper.compose(), and assert result.document['name']=='my-marketplace'. -- Evidence outcome=missing on an integration-tier user promise: removing sanitize_marketplace_name() from either mapper's compose() passes every existing test today. This is the strongest structural gap in the PR -- a regression trap is required before next release.
  2. [CLI Logging Expert] Emit a MapperResult.warnings entry in both ClaudeMarketplaceMapper and CodexMarketplaceMapper when sanitized != config.name; add a distinct warning for the fallback path. CodexMarketplaceMapper also needs to pass the warnings tuple to MapperResult. -- Five panelists converged on this independently -- the highest-convergence signal in the panel. The warnings channel is already wired to CLI output by builder.py. Silent mutation of a user-declared name breaks the "no surprises" contract.
  3. [DevX UX Expert] Replace return result or "marketplace" in sanitize_marketplace_name() with a raised BuildError when the sanitized result is empty after stripping non-alphanumeric characters. -- Two panelists (devx-ux, supply-chain-security) independently flagged the all-punctuation fallback as a silent registry collision risk. Fail-open to a generic name is a defect; build-time failure is the correct gate.
  4. [Doc Writer] Add a CHANGELOG.md [Unreleased] ### Fixed entry for the kebab-case normalization and correct the "only structural transform" claim in docs/src/content/docs/producer/publish-to-a-marketplace.md (line 119-121). -- The PR changes user-observable output for any user whose project name contains dots, underscores, or uppercase. A CHANGELOG entry is the standard discovery mechanism and the doc claim is now factually incorrect.
  5. [Supply Chain Security Expert] Add a lightweight upstream format guard in yml_schema.py requiring that config.name contains at least one alphanumeric character, surfacing the error at config-load time rather than at compose time. -- An all-punctuation name currently satisfies schema validation and flows silently to the sanitizer. Early detection gives the author a cleaner error message and closes the gap that lets malformed names reach the output boundary at all.

Architecture

classDiagram
    direction LR
    class MarketplaceOutputMapper {
        <<Abstract>>
        +uses_remote_metadata bool
        +compose(config, resolved, remote_metadata) MapperResult
    }
    class ClaudeMarketplaceMapper {
        <<ConcreteMapper>>
        +uses_remote_metadata = True
        +compose(config, resolved, remote_metadata) MapperResult
    }
    class CodexMarketplaceMapper {
        <<ConcreteMapper>>
        +compose(config, resolved, remote_metadata) MapperResult
    }
    class MapperResult {
        <<ValueObject>>
        +document dict
        +warnings tuple
        +diagnostics tuple
    }
    class MarketplaceConfig {
        <<ValueObject>>
        +name str
        +packages tuple
    }
    class sanitize_marketplace_name {
        <<PureFunction>>
        +sanitize_marketplace_name(name str) str
    }
    MarketplaceOutputMapper <|-- ClaudeMarketplaceMapper
    MarketplaceOutputMapper <|-- CodexMarketplaceMapper
    ClaudeMarketplaceMapper ..> MapperResult : produces
    CodexMarketplaceMapper ..> MapperResult : produces
    ClaudeMarketplaceMapper ..> MarketplaceConfig : reads
    CodexMarketplaceMapper ..> MarketplaceConfig : reads
    ClaudeMarketplaceMapper ..> sanitize_marketplace_name : output boundary
    CodexMarketplaceMapper ..> sanitize_marketplace_name : output boundary
    class ClaudeMarketplaceMapper:::touched
    class CodexMarketplaceMapper:::touched
    class sanitize_marketplace_name:::touched
    classDef touched fill:#fff3b0,stroke:#d47600
Loading
flowchart TD
    A([apm marketplace build]) --> B[MarketplaceBuilder.write_output]
    B --> C[MarketplaceBuilder.compose_output]
    C --> D[MarketplaceBuilder._map_output]
    D --> E{profile.mapper}
    E -- claude --> F[ClaudeMarketplaceMapper.compose]
    E -- codex --> G[CodexMarketplaceMapper.compose]
    F --> H["sanitize_marketplace_name(config.name) NEW"]
    G --> I["sanitize_marketplace_name(config.name) NEW"]
    H --> J[doc.name = sanitized]
    I --> K["doc.name = sanitized / doc.interface.displayName = config.name original"]
    J --> L["MapperResult(doc + warnings + diagnostics)"]
    K --> M["MapperResult(doc only) - warnings channel empty"]
    L --> N["FS atomic_write marketplace.json"]
    M --> N
Loading

Recommendation

Approve and merge. The core fix is correct, the implementation is clean, and the enterprise adoption unlock is real. Open a tracking issue immediately for (1) the compose()-tier regression trap -- the most urgent item since the sanitize call can be silently deleted today with all tests still passing -- and (2) the silent mutation warning, which has five-panelist consensus and a clear implementation path through MapperResult.warnings. The CHANGELOG entry and doc correction are lightweight and must land in the same release. The BuildError for empty sanitization and the upstream yml_schema guard are follow-up PRs in the same sprint.


Full per-persona findings

Python Architect

  • [recommended] No diagnostic emitted when sanitize_marketplace_name mutates the marketplace name at src/apm_cli/marketplace/output_mappers.py:86
    When sanitize_marketplace_name(config.name) != config.name, both mappers write the transformed name to marketplace.json with no user-visible signal. The diagnostics list + _duplicate_name_warnings pattern is the correct channel.
    Suggested: In ClaudeMarketplaceMapper.compose(), compare sanitized against config.name and append a BuildDiagnostic(level='warning') when they differ. In CodexMarketplaceMapper.compose(), add a local warnings list and MapperResult(doc, tuple(warnings)).

  • [nit] PR title/body uses "sanitise" (British) but the function is sanitize_marketplace_name (American) at src/apm_cli/marketplace/output_mappers.py:20
    Minor cosmetic inconsistency between PR title and actual symbol name. All existing helpers in the codebase use the American spelling.

CLI Logging Expert

  • [recommended] Name sanitization is fully silent when config.name is mutated at src/apm_cli/marketplace/output_mappers.py
    When sanitize_marketplace_name() changes the declared name, neither mapper emits any user-visible diagnostic. The user has no way to know the emitted marketplace.json uses a different name than their apm.yml. The correct channel (MapperResult.warnings) already exists and is used by duplicate-name logic.
    Suggested: In both mappers, collect a string warning when sanitized != config.name and include it in MapperResult.warnings. CodexMarketplaceMapper needs to be updated to pass the warnings tuple.

  • [recommended] Fallback to literal "marketplace" when entire name strips away is completely silent at src/apm_cli/marketplace/output_mappers.py:37
    sanitize_marketplace_name('...') returns 'marketplace' with no feedback. This is a complete silent identity replacement with a generic string.
    Suggested: Treat the fallback branch as a louder warning or error: "Marketplace name reduces to empty after sanitization -- using fallback name 'marketplace'. Update name in apm.yml."

  • [nit] Test coverage does not assert warning emission from compose() at tests/unit/marketplace/test_name_sanitisation.py
    No tests check that MapperResult.warnings contains the expected message when config.name != sanitized.

DevX UX Expert

  • [recommended] Name sanitization is completely silent -- no warning emitted when config.name is rewritten at src/apm_cli/marketplace/output_mappers.py
    Both mappers call sanitize_marketplace_name() but neither emits a warning when the sanitized value differs from config.name. The warnings channel is plumbed all the way to CLI output by builder.py.
    Suggested: If sanitized != config.name, append a BuildDiagnostic warning.

  • [recommended] Fallback to literal "marketplace" on empty sanitization is silent and introduces a collision risk at src/apm_cli/marketplace/output_mappers.py
    Two entirely different marketplace configs whose names consist solely of non-alphanumeric characters would both produce name: "marketplace" in their output JSON. The appropriate UX is to raise a BuildError.
    Suggested: Replace return result or "marketplace" with a raised BuildError when result is empty.

  • [nit] CodexMarketplaceMapper returns MapperResult(doc) with no warnings tuple -- asymmetry makes future warning additions easy to miss at src/apm_cli/marketplace/output_mappers.py
    Structural asymmetry between ClaudeMarketplaceMapper (returns warnings+diagnostics) and CodexMarketplaceMapper (returns doc only).
    Suggested: Change CodexMarketplaceMapper to always pass warnings and diagnostics even if empty.

Supply Chain Security Expert

  • [recommended] Silent fallback to generic name "marketplace" when sanitization yields empty string at src/apm_cli/marketplace/output_mappers.py
    sanitize_marketplace_name() returns 'marketplace' when every character is non-alphanumeric. Build proceeds without error or BuildDiagnostic. A BuildError should be raised so the author is forced to supply a name with at least one alphanumeric character.
    Suggested: Replace return result or "marketplace" with a BuildError raised when result is empty.

  • [recommended] Lossy sanitization applied silently with no warning -- collision surface undetected at src/apm_cli/marketplace/output_mappers.py
    Two apm.yml files with distinct names that happen to normalize identically will produce competing marketplace.json files with the same name field, giving consumers no signal. A BuildDiagnostic(level='warning') should be emitted whenever the sanitized value differs from the input.
    Suggested: After computing sanitized, add conditional warning when sanitized != config.name.

  • [nit] No upstream format guard on config.name in yml_schema allows all-punctuation names through schema validation at src/apm_cli/marketplace/yml_schema.py
    _require_str only asserts non-empty string. A name like '---' satisfies schema validation and flows silently to the sanitizer.
    Suggested: Add a check that name contains at least one alphanumeric character at config-load time.

OSS Growth Hacker

  • [recommended] CHANGELOG [Unreleased] section has no entry for this fix at CHANGELOG.md
    Users who hit the Copilot App "must be kebab-case" validation error will search release notes for relief. With no CHANGELOG entry, they cannot discover this fix even after upgrading.
    Suggested: Add under [Unreleased] ### Fixed: "Marketplace names containing dots, underscores, or other non-kebab-case characters are now automatically normalized to kebab-case in marketplace.json output, resolving the Copilot App validation error."

  • [recommended] Silent name transformation gives users no feedback when their marketplace name is rewritten at src/apm_cli/marketplace/output_mappers.py
    When sanitize_marketplace_name rewrites the name, the user sees nothing in CLI output. A conditional info line at build time would build trust with enterprise users.
    Suggested: Emit a conditional logger/diagnostic line when config.name != sanitized.

  • [nit] Test file and PR title use British "sanitise" while codebase uses American "sanitize" at tests/unit/marketplace/test_name_sanitisation.py
    Cosmetic inconsistency; does not affect correctness but could confuse grep searches.
    Suggested: Rename to test_name_sanitization.py and align PR title with American spelling.

Doc Writer

  • [recommended] publish-to-a-marketplace.md claims "that rename is the only structural transform apm pack performs" -- this PR breaks that claim at docs/src/content/docs/producer/publish-to-a-marketplace.md:119
    This PR adds a second output-level transform: the top-level name field is silently normalized to kebab-case. The doc is now factually incorrect.
    Suggested: Update to mention the kebab-case normalization of the name field as a second output-level transform performed by apm pack.

  • [recommended] CHANGELOG.md [Unreleased] has no entry for this fix at CHANGELOG.md:8
    This PR changes user-observable output -- any user whose project name contains dots, underscores, or uppercase will see their marketplace.json name field silently change after upgrading. A CHANGELOG entry is the standard mechanism for communicating such changes.
    Suggested: Add to the existing ### Fixed block: "apm pack now normalizes the marketplace name field to kebab-case in generated marketplace.json output."

  • [nit] Pitfalls section should call out name normalization at docs/src/content/docs/producer/publish-to-a-marketplace.md:229
    The Pitfalls section lists non-obvious behaviors. Name normalization is equally surprising to users.
    Suggested: Add a pitfall bullet explaining that project names with dots, underscores, or uppercase are normalized to kebab-case in output JSON.

Test Coverage Expert

  • [recommended] No regression trap for sanitize_marketplace_name() call-site in compose() -- removing the call from either mapper would silently pass every existing test at tests/unit/marketplace/test_name_sanitisation.py
    Unit tests confirm the function works in isolation (16 cases, tier: unit). But zero tests call ClaudeMarketplaceMapper.compose() or CodexMarketplaceMapper.compose() directly. All integration fixtures use names already in valid kebab-case (acme-tools, test-marketplace). Because sanitize(kebab-name)==kebab-name, removing the sanitize call from either compose() leaves all tests green.
    Proof (missing at integration-with-fixtures): tests/unit/marketplace/test_output_mappers.py -- proves: ClaudeMarketplaceMapper.compose() and CodexMarketplaceMapper.compose() write the sanitized name (not raw config.name) to the output document when the name contains dots or underscores
    Suggested: Add a test that constructs a MarketplaceConfig with name='my.marketplace', calls ClaudeMarketplaceMapper().compose(...), and asserts result.document['name']=='my-marketplace'. Mirror for CodexMarketplaceMapper.

  • [nit] Unicode marketplace names absent from sanitize_marketplace_name() unit suite at tests/unit/marketplace/test_name_sanitisation.py
    Non-ASCII characters (e.g., accented chars, CJK) are not tested. The regex [^a-z0-9] deterministically collapses them to hyphens, but the assertion is absent. If the regex were changed to \W, unicode behavior would silently shift.

Auth Expert -- inactive

No auth, token, credential, or host-classification code was touched in this PR.

Performance Expert -- inactive

PR touches only src/apm_cli/marketplace/output_mappers.py (regex string normalization) and tests/unit/marketplace/test_name_sanitisation.py -- no download, cache, install pipeline, transport, or materialization surface is affected.

This panel is advisory. It does not block merge. Re-apply the
panel-review label after addressing feedback to re-run.

Generated by PR Review Panel for issue #2008 · 535.8 AIC · ⌖ 18.3 AIC · ⊞ 5.5K ·

danielmeppiel and others added 4 commits July 3, 2026 20:53
The existing suite unit-tests sanitize_marketplace_name in isolation but
nothing proves the normalization actually reaches the on-disk artifact.
This adds an end-to-end integration test that writes a marketplace.yml
with a non-kebab-case name (My.Marketplace_Name), runs the full build
pipeline (load -> resolve -> compose -> write), and asserts the emitted
Claude marketplace.json and the composed Codex document both carry the
kebab-case name while internal package names and the Codex
interface.displayName keep the original. Mutation-break gate verified:
removing the sanitize call in both mappers fails the two name assertions.

Addresses shepherd fold: empirical e2e proof for behavior this PR introduces.

Co-authored-by: Copilot <[email protected]>
Add an [Unreleased] > Fixed entry for the user-observable behavior change
in this PR (marketplace.json name is now normalized to kebab-case for
Copilot App compatibility). Keep-a-Changelog format, references #2008.

Addresses shepherd fold: CHANGELOG entry for this change.

Co-authored-by: Copilot <[email protected]>
When apm pack normalises the marketplace name to kebab-case, surface a
non-blocking build warning naming the original and sanitised values so
authors see the transform instead of it being silent. Extract a shared
_sanitized_name_with_diagnostic helper wired into both the Claude and
Codex output mappers, extend the e2e proof with diagnostic + all-special
fallback cases, and document the normalisation in the publish guide and
manifest schema reference.

Co-authored-by: Copilot <[email protected]>
Terminal apm-review-panel pass surfaced only nits (no blocking/recommended).
Fold the cheap in-scope items:
- pre-compile the sanitiser regexes at module scope (py-arch idiom match)
- fix the warning to point at "your marketplace config" not marketplace.yml,
  since the recommended location is the apm.yml marketplace: block (devx-ux)
- document the intentional many-to-one mapping in the docstring (security)
- lead the CHANGELOG entry with user value + note the warning (growth)
- mention the rewrite warning in the publish guide, split the dense
  sentence, and add a kebab-case pointer on the schema 3.1 name row (doc)

Deferred (scope-crossing, boundary-noted in the PR advisory): typing
BuildDiagnostic.level as Literal, and widening the compose-only
compose_codex_marketplace_json helper to a 3-tuple (the write path already
surfaces the diagnostic).

Co-authored-by: Copilot <[email protected]>
@danielmeppiel

Copy link
Copy Markdown
Collaborator

APM Review Panel: ship_now

Ship: kebab-case sanitiser closes the silent-rejection cliff for marketplace producers with proven mutation-break coverage and zero blocking findings.

cc @sergio-sisternes-epam @danielmeppiel -- a fresh advisory pass is ready for your review.

All nine panelists converged on ship. The two active architectural voices (python-architect, cli-logging-expert) confirmed the shared-helper extraction is well-motivated and follows the existing collect-then-render diagnostic pattern. Supply-chain-security certified a net-positive: the sanitiser narrows the injection surface on emitted names while the raw name stays internal-only. The test-coverage-expert returned zero findings with six evidence blocks at integration-with-fixtures tier, all outcome=passed, and the mutation-break gate is proven -- removing the sanitize guard fails name-assertion tests; neutering the diagnostic condition fails test_rewrite_emits_warning_diagnostic. This is the empirical-proof bar the maintainer asked for.

No material dissent exists. The two deferred nits are cleanly out of scope: (1) typing BuildDiagnostic.level as Literal touches a pre-existing shared type across the codebase, not this PR's surface; (2) widening the compose-only preview helper to surface diagnostics is a separate refactor since the real write path already renders them. Both are bounded follow-ups with no user-facing regression risk if deferred.

Strategically, this change reinforces APM's "just works" positioning for the producer publish flow. A marketplace.json that silently fails on the Copilot App side is exactly the kind of first-run cliff that kills adoption before a user ever sees value. Closing it with a warn-not-error approach (npm-like ergonomics per devx-ux-expert) is the right trade-off: producers are informed, never blocked.

Aligned with: Pragmatic as npm (warn-not-error mirrors npm normalize-package-name; producers informed, never blocked); Portable by manifest (raw manifest name preserved for internal resolution, only the emitted artifact is sanitised); Multi-harness / multi-host (Claude and Codex mappers apply identical sanitisation via one shared helper); OSS community-driven (real community pain point shipped with full empirical proof); Secure by default (emitted name constrained to [a-z0-9-], closing injection surface on the published artifact).

Growth signal. Closes the "marketplace.json silently rejected by Copilot App" first-run cliff for producers. Amplify in release notes as a "just works" DX win -- producers no longer need to manually kebab-case their config name to get a successful publish.

Panel summary

Persona B R N Takeaway
Python Architect 0 0 2 Architecturally clean output-boundary sanitization; shared-helper extraction well-motivated; diagnostic aggregation follows existing collect-then-render pattern.
CLI Logging Expert 0 0 1 Diagnostic wording clear, actionable, ASCII [!]-prefixed, names both values; level=warning correct for a silent rewrite.
DevX UX Expert 0 0 1 Warn-not-error is defensible npm-like ergonomics; fallback and diagnostic UX solid.
Supply Chain Security Expert 0 0 1 Net-positive: sanitiser closes injection surface on emitted name; no blocking supply-chain risk.
OSS Growth Hacker 0 0 2 Removes a silent producer-funnel footgun; docs + diagnostic coverage solid.
Doc Writer 0 0 2 Prose matches sanitiser; anchor #72-block-fields resolves; two discoverability nits.
Test Coverage Expert 0 0 0 All behavioral surfaces have regression-trap tests at integration-with-fixtures tier; 22 tests pass; mutation-break confirmed. Ship.

B = blocking-severity findings, R = recommended, N = nits.
Counts are signal strength, not gates. The maintainer ships. All eight in-scope nits were folded into this PR (see "Folded in this run"); two out-of-scope nits are deferred.

Top 2 follow-ups

  1. [Python Architect] Type BuildDiagnostic.level as a Literal -- pre-existing shared type used across the codebase; typing it narrows the contract but touches many call-sites. Bounded refactor, no user-facing urgency.
  2. [CLI Logging Expert] Surface diagnostics from the compose-only compose_codex_marketplace_json helper -- it returns a 2-tuple and drops the diagnostic list. The real write path already surfaces them; widening the preview helper is a separate refactor with no regression risk if deferred.

Architecture

classDiagram
    direction LR
    class MarketplaceOutputMapper {
      <<AbstractBase>>
      +compose(config, resolved, remote_metadata) MapperResult
    }
    class ClaudeMarketplaceMapper {
      <<ConcreteStrategy>>
      +compose(...) MapperResult
    }
    class CodexMarketplaceMapper {
      <<ConcreteStrategy>>
      +compose(...) MapperResult
    }
    class MapperResult {
      <<ValueObject>>
      +document dict
      +warnings tuple
      +diagnostics tuple~BuildDiagnostic~
    }
    class BuildDiagnostic {
      <<ValueObject>>
      +level str
      +message str
    }
    class MarketplaceBuilder {
      <<Facade>>
      +build() BuildReport
    }
    class BuildReport {
      <<ValueObject>>
      +outputs tuple~MarketplaceOutputReport~
      +diagnostics tuple~BuildDiagnostic~
    }
    class sanitize_marketplace_name {
      <<Pure>>
      +__call__(name) str
    }
    class _sanitized_name_with_diagnostic {
      <<Pure>>
      +__call__(config_name) tuple
    }
    MarketplaceOutputMapper <|-- ClaudeMarketplaceMapper
    MarketplaceOutputMapper <|-- CodexMarketplaceMapper
    ClaudeMarketplaceMapper ..> MapperResult : returns
    CodexMarketplaceMapper ..> MapperResult : returns
    MapperResult *-- BuildDiagnostic : contains
    MarketplaceBuilder *-- MarketplaceOutputMapper : delegates
    MarketplaceBuilder ..> BuildReport : returns
    BuildReport o-- BuildDiagnostic : aggregates
    note for _sanitized_name_with_diagnostic "Shared helper: calls sanitize_marketplace_name,\nemits BuildDiagnostic when rewrite occurs"
    note for MarketplaceBuilder "Collect-then-render: diagnostics\naccumulate across mappers into BuildReport"
    class ClaudeMarketplaceMapper:::touched
    class CodexMarketplaceMapper:::touched
    class sanitize_marketplace_name:::touched
    class _sanitized_name_with_diagnostic:::touched
    classDef touched fill:#fff3b0,stroke:#d47600
Loading
flowchart TD
    A["apm.yml: name = 'My.Marketplace_Name'"] --> B["MarketplaceBuilder.build()"]
    B --> C["ClaudeMarketplaceMapper.compose() / CodexMarketplaceMapper.compose()"]
    C --> D["_sanitized_name_with_diagnostic(config.name)"]
    D --> E["sanitize_marketplace_name(name)"]
    E --> F{"sanitized == config_name?"}
    F -- Yes --> G["Return sanitized, diagnostics=[]"]
    F -- No --> H["Emit BuildDiagnostic level=warning\noriginal + sanitized in message"]
    H --> I["Return sanitized, diagnostics=[diag]"]
    G --> J["doc['name'] = sanitized"]
    I --> J
    J --> K{"Mapper type?"}
    K -- Claude --> L["diagnostics = list(name_diagnostics) +\nper-plugin diagnostics"]
    K -- Codex --> M["doc['interface']['displayName'] = config.name\nMapperResult(doc, (), tuple(name_diagnostics))"]
    L --> N["MapperResult(doc, warnings, tuple(diagnostics))"]
    M --> O["[FS] MarketplaceBuilder writes marketplace.json"]
    N --> O
    O --> P["BuildReport.diagnostics aggregates all outputs"]
Loading

Recommendation

Zero blocking findings, zero recommended findings, all in-scope nits folded, mutation-break gate proven, lint silent, 1484+22 tests green, and test-coverage-expert certified ship at integration-with-fixtures tier. The two deferred nits are cleanly out of scope with no user-facing risk. Ship.


Folded in this run

  • (user-request) e2e integration test proving the full build emits a sanitized kebab-case name (empirical proof) -- resolved in 151fdd5d.
  • (shepherd) CHANGELOG [Unreleased] > Fixed entry for the behavior change -- resolved in b7139be8.
  • (devx-ux-expert + cli-logging-expert) non-blocking build warning naming original + emitted name on rewrite, via a shared _sanitized_name_with_diagnostic helper wired into both mappers -- resolved in 52ca9b51.
  • (doc-writer) docs drift: publish guide + manifest schema now describe the kebab-case normalisation -- resolved in 52ca9b51.
  • (python-architect) pre-compile the sanitiser regexes at module scope (codebase idiom) -- resolved in 3a3ba870.
  • (devx-ux-expert) warning cited marketplace.yml; retargeted to "your marketplace config" since the recommended location is the apm.yml marketplace: block -- resolved in 3a3ba870.
  • (supply-chain-security-expert) documented the intentional many-to-one mapping in the sanitiser docstring -- resolved in 3a3ba870.
  • (oss-growth-hacker) CHANGELOG re-led with user value; publish guide now names the rewrite warning -- resolved in 3a3ba870.
  • (doc-writer) added a kebab-case pointer on the schema 3.1 name row; split the dense producer sentence -- resolved in 3a3ba870.

Copilot signals reviewed

  • src/apm_cli/marketplace/output_mappers.py -- LEGIT (sanitise -> sanitize naming): already resolved by the author at HEAD 0e40142e before this shepherding pass.
  • src/apm_cli/marketplace/output_mappers.py -- LEGIT (docstring wording): already resolved by the author at HEAD 0e40142e before this shepherding pass.

Deferred (out-of-scope follow-ups)

These items were surfaced by the panel but cross the stated scope of this PR. Each suggests a separate follow-up.

  • (python-architect) Type BuildDiagnostic.level as Literal["warning", "verbose"] -- scope boundary: a pre-existing shared type in diagnostics.py used across the marketplace subsystem, outside this PR's name-sanitisation surface.
  • (cli-logging-expert) Widen the compose-only compose_codex_marketplace_json helper to surface diagnostics -- scope boundary: the user-facing write path (write_codex_marketplace_json -> write_output -> compose_output) already surfaces the diagnostic; changing the preview-helper signature is a separate refactor.

Regression-trap evidence (mutation-break gate)

  • tests/integration/marketplace/test_name_sanitisation_e2e.py::TestClaudeOutputSanitisesName::test_written_json_name_is_kebab_case -- removed the sanitize_marketplace_name guard from both mappers; the name-assertion tests FAILED as expected; guard restored.
  • tests/integration/marketplace/test_name_sanitisation_e2e.py::TestClaudeOutputSanitisesName::test_rewrite_emits_warning_diagnostic -- neutered the if sanitized != config_name diagnostic condition; the diagnostic test FAILED as expected; condition restored.

Lint contract

uv run --extra dev ruff check src/ tests/ and uv run --extra dev ruff format --check src/ tests/ both silent. Pylint R0801 duplication guard: 10.00/10.

CI

All required checks green on 3a3ba870: Lint, Build & Test Shard 1 (Linux), Build & Test Shard 2 (Linux), gate, Coverage Combine (Linux), PR Binary Smoke (Linux), Spec conformance gate, CodeQL (after 0 CI fix iterations).

Mergeability status

PR head SHA CEO stance iters folds defers Copilot rounds CI mergeable mergeStateStatus notes
#2008 3a3ba870 ship_now 1 9 2 1 green MERGEABLE BLOCKED BLOCKED = awaiting required review approval; CI is green, no merge conflict.

Convergence

1 outer iteration; 1 Copilot round. Final panel stance: ship_now.

Ready for maintainer review.


Full per-persona findings

Python Architect

  • [nit] Pre-compile the two regexes used in sanitize_marketplace_name at src/apm_cli/marketplace/output_mappers.py:38
    The two re.sub calls re-compile on every invocation; the idiomatic APM pattern is module-level pre-compiled patterns. FOLDED in 3a3ba870.
  • [nit] Consider a Literal type for BuildDiagnostic.level at src/apm_cli/marketplace/diagnostics.py:12
    Only valid values are "warning"/"verbose"; a Literal annotation would catch typos at type-check time. Pre-existing gap. DEFERRED (out of scope).

CLI Logging Expert

  • [nit] compose_codex_marketplace_json drops the new diagnostic on compose-only callers at src/apm_cli/marketplace/builder.py:1140
    The compose-only method returns (document, warnings) without diagnostics. The write path (write_codex_marketplace_json -> write_output -> compose_output) surfaces them correctly. DEFERRED (out of scope).

DevX UX Expert

  • [nit] Warning message hardcoded marketplace.yml but most users configure in apm.yml at src/apm_cli/marketplace/output_mappers.py:63
    Retargeted the recovery hint to "your marketplace config". FOLDED in 3a3ba870.

Supply Chain Security Expert

  • [nit] Many-to-one collision not documented as a known limitation at src/apm_cli/marketplace/output_mappers.py:20
    Distinct raw names can map to the same kebab name; not exploitable (output-only, not identity), but worth documenting. FOLDED in 3a3ba870.

OSS Growth Hacker

  • [nit] CHANGELOG entry buried the user payoff behind implementation detail at CHANGELOG.md:12
    Re-led with user value. FOLDED in 3a3ba870.
  • [nit] Publish guide did not call out the warning diagnostic at docs/src/content/docs/producer/publish-to-a-marketplace.md:121
    Added a sentence naming the rewrite warning. FOLDED in 3a3ba870.

Auth Expert -- inactive

PR touches only marketplace/output_mappers.py, two test files, CHANGELOG.md, and two docs pages -- no auth, token, credential, or remote-host surface affected.

Doc Writer

  • [nit] Sanitisation note lived only on the 7.2 override name row; top-level 3.1 name was silent at docs/src/content/docs/reference/manifest-schema.md:809
    Added a one-clause pointer on the 3.1 name row. FOLDED in 3a3ba870.
  • [nit] Producer paragraph packed the whole rule into one multi-clause sentence at docs/src/content/docs/producer/publish-to-a-marketplace.md:118
    Split the sentence. FOLDED in 3a3ba870.

Supply / Test Coverage Expert

No findings. All behavioral surfaces (Claude name sanitisation, Codex name + displayName preservation, diagnostic emission, no-diagnostic-on-kebab, all-special fallback) have regression-trap tests at integration-with-fixtures tier via the full MarketplaceBuilder pipeline. 22 tests pass in 2.14s. Mutation-break gate confirmed.
Proof (passed): tests/integration/marketplace/test_name_sanitisation_e2e.py::TestClaudeOutputSanitisesName::test_written_json_name_is_kebab_case -- proves: compiled marketplace.json carries a kebab-case name the Copilot App accepts [devx,portability-by-manifest]
assert data["name"] == EXPECTED_NAME

Performance Expert -- inactive

PR touches only marketplace/output_mappers.py, tests, CHANGELOG.md, and docs -- no install, deps, cache, pipeline, resolve, or perf-instrumentation surface.

This panel is advisory. It does not block merge. Re-apply the panel-review label after addressing feedback to re-run.

@danielmeppiel danielmeppiel disabled auto-merge July 3, 2026 19:29
@danielmeppiel danielmeppiel merged commit 13e96d1 into main Jul 3, 2026
15 checks passed
@danielmeppiel danielmeppiel deleted the sergio-sisternes-epam-fix-marketplace-name-sanitisation branch July 3, 2026 19:29
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.

4 participants