fix: Sanitise marketplace name to kebab-case in output mappers#2008
Conversation
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]>
There was a problem hiding this comment.
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.nameinto 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. |
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]>
APM Review Panel:
|
| 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
- [Test Coverage Expert] Add compose()-tier regression tests that construct a
MarketplaceConfigwithname='my.marketplace', callClaudeMarketplaceMapper.compose()andCodexMarketplaceMapper.compose(), and assertresult.document['name']=='my-marketplace'. -- Evidenceoutcome=missingon an integration-tier user promise: removingsanitize_marketplace_name()from either mapper'scompose()passes every existing test today. This is the strongest structural gap in the PR -- a regression trap is required before next release. - [CLI Logging Expert] Emit a
MapperResult.warningsentry in bothClaudeMarketplaceMapperandCodexMarketplaceMapperwhensanitized != config.name; add a distinct warning for the fallback path.CodexMarketplaceMapperalso needs to pass the warnings tuple toMapperResult. -- Five panelists converged on this independently -- the highest-convergence signal in the panel. The warnings channel is already wired to CLI output bybuilder.py. Silent mutation of a user-declared name breaks the "no surprises" contract. - [DevX UX Expert] Replace
return result or "marketplace"insanitize_marketplace_name()with a raisedBuildErrorwhen 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. - [Doc Writer] Add a
CHANGELOG.md[Unreleased]### Fixedentry for the kebab-case normalization and correct the "only structural transform" claim indocs/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. - [Supply Chain Security Expert] Add a lightweight upstream format guard in
yml_schema.pyrequiring thatconfig.namecontains 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
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
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
Whensanitize_marketplace_name(config.name) != config.name, both mappers write the transformed name tomarketplace.jsonwith no user-visible signal. The diagnostics list +_duplicate_name_warningspattern is the correct channel.
Suggested: InClaudeMarketplaceMapper.compose(), compare sanitized againstconfig.nameand append aBuildDiagnostic(level='warning')when they differ. InCodexMarketplaceMapper.compose(), add a local warnings list andMapperResult(doc, tuple(warnings)). -
[nit] PR title/body uses "sanitise" (British) but the function is
sanitize_marketplace_name(American) atsrc/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
Whensanitize_marketplace_name()changes the declared name, neither mapper emits any user-visible diagnostic. The user has no way to know the emittedmarketplace.jsonuses a different name than theirapm.yml. The correct channel (MapperResult.warnings) already exists and is used by duplicate-name logic.
Suggested: In both mappers, collect a string warning whensanitized != config.nameand include it inMapperResult.warnings.CodexMarketplaceMapperneeds 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 thatMapperResult.warningscontains the expected message whenconfig.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 callsanitize_marketplace_name()but neither emits a warning when the sanitized value differs fromconfig.name. The warnings channel is plumbed all the way to CLI output bybuilder.py.
Suggested: Ifsanitized != config.name, append aBuildDiagnosticwarning. -
[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 producename: "marketplace"in their output JSON. The appropriate UX is to raise aBuildError.
Suggested: Replacereturn result or "marketplace"with a raisedBuildErrorwhen result is empty. -
[nit]
CodexMarketplaceMapperreturnsMapperResult(doc)with no warnings tuple -- asymmetry makes future warning additions easy to miss atsrc/apm_cli/marketplace/output_mappers.py
Structural asymmetry betweenClaudeMarketplaceMapper(returns warnings+diagnostics) andCodexMarketplaceMapper(returns doc only).
Suggested: ChangeCodexMarketplaceMapperto 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 orBuildDiagnostic. ABuildErrorshould be raised so the author is forced to supply a name with at least one alphanumeric character.
Suggested: Replacereturn result or "marketplace"with aBuildErrorraised when result is empty. -
[recommended] Lossy sanitization applied silently with no warning -- collision surface undetected at
src/apm_cli/marketplace/output_mappers.py
Twoapm.ymlfiles with distinct names that happen to normalize identically will produce competingmarketplace.jsonfiles with the same name field, giving consumers no signal. ABuildDiagnostic(level='warning')should be emitted whenever the sanitized value differs from the input.
Suggested: After computingsanitized, add conditional warning whensanitized != 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_stronly 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 inmarketplace.jsonoutput, 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
Whensanitize_marketplace_namerewrites 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 whenconfig.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 totest_name_sanitization.pyand align PR title with American spelling.
Doc Writer
-
[recommended]
publish-to-a-marketplace.mdclaims "that rename is the only structural transform apm pack performs" -- this PR breaks that claim atdocs/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 byapm 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 theirmarketplace.jsonname field silently change after upgrading. A CHANGELOG entry is the standard mechanism for communicating such changes.
Suggested: Add to the existing### Fixedblock: "apm packnow normalizes the marketplacenamefield to kebab-case in generatedmarketplace.jsonoutput." -
[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 incompose()-- removing the call from either mapper would silently pass every existing test attests/unit/marketplace/test_name_sanitisation.py
Unit tests confirm the function works in isolation (16 cases, tier: unit). But zero tests callClaudeMarketplaceMapper.compose()orCodexMarketplaceMapper.compose()directly. All integration fixtures use names already in valid kebab-case (acme-tools,test-marketplace). Becausesanitize(kebab-name)==kebab-name, removing the sanitize call from eithercompose()leaves all tests green.
Proof (missing at integration-with-fixtures):tests/unit/marketplace/test_output_mappers.py-- proves:ClaudeMarketplaceMapper.compose()andCodexMarketplaceMapper.compose()write the sanitized name (not rawconfig.name) to the output document when the name contains dots or underscores
Suggested: Add a test that constructs aMarketplaceConfigwithname='my.marketplace', callsClaudeMarketplaceMapper().compose(...), and assertsresult.document['name']=='my-marketplace'. Mirror forCodexMarketplaceMapper. -
[nit] Unicode marketplace names absent from
sanitize_marketplace_name()unit suite attests/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 · ◷
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]>
APM Review Panel:
|
| 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
- [Python Architect] Type
BuildDiagnostic.levelas aLiteral-- pre-existing shared type used across the codebase; typing it narrows the contract but touches many call-sites. Bounded refactor, no user-facing urgency. - [CLI Logging Expert] Surface diagnostics from the compose-only
compose_codex_marketplace_jsonhelper -- 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
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"]
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] > Fixedentry for the behavior change -- resolved inb7139be8. - (devx-ux-expert + cli-logging-expert) non-blocking build warning naming original + emitted name on rewrite, via a shared
_sanitized_name_with_diagnostichelper wired into both mappers -- resolved in52ca9b51. - (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 theapm.ymlmarketplace:block -- resolved in3a3ba870. - (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
namerow; split the dense producer sentence -- resolved in3a3ba870.
Copilot signals reviewed
src/apm_cli/marketplace/output_mappers.py-- LEGIT (sanitise -> sanitize naming): already resolved by the author at HEAD0e40142ebefore this shepherding pass.src/apm_cli/marketplace/output_mappers.py-- LEGIT (docstring wording): already resolved by the author at HEAD0e40142ebefore 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.levelasLiteral["warning", "verbose"]-- scope boundary: a pre-existing shared type indiagnostics.pyused across the marketplace subsystem, outside this PR's name-sanitisation surface. - (cli-logging-expert) Widen the compose-only
compose_codex_marketplace_jsonhelper 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 thesanitize_marketplace_nameguard 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 theif sanitized != config_namediagnostic 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_nameatsrc/apm_cli/marketplace/output_mappers.py:38
The twore.subcalls re-compile on every invocation; the idiomatic APM pattern is module-level pre-compiled patterns. FOLDED in3a3ba870. - [nit] Consider a
Literaltype forBuildDiagnostic.levelatsrc/apm_cli/marketplace/diagnostics.py:12
Only valid values are "warning"/"verbose"; aLiteralannotation would catch typos at type-check time. Pre-existing gap. DEFERRED (out of scope).
CLI Logging Expert
- [nit]
compose_codex_marketplace_jsondrops the new diagnostic on compose-only callers atsrc/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.ymlbut most users configure inapm.ymlatsrc/apm_cli/marketplace/output_mappers.py:63
Retargeted the recovery hint to "your marketplace config". FOLDED in3a3ba870.
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 in3a3ba870.
OSS Growth Hacker
- [nit] CHANGELOG entry buried the user payoff behind implementation detail at
CHANGELOG.md:12
Re-led with user value. FOLDED in3a3ba870. - [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 in3a3ba870.
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
namerow; top-level 3.1namewas silent atdocs/src/content/docs/reference/manifest-schema.md:809
Added a one-clause pointer on the 3.1namerow. FOLDED in3a3ba870. - [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 in3a3ba870.
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.
Description
Repository names containing dots, underscores, or other non-kebab-case characters (e.g.
my.marketplace) flow intomarketplace.jsonverbatim. 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 composingmarketplace.json. The function lowercases, replaces non-alphanumeric characters with hyphens, collapses consecutive hyphens, and strips leading/trailing hyphens.Applied in both
ClaudeMarketplaceMapperandCodexMarketplaceMapper. Internal resolution, registry lookups, and install flows remain untouched -- the original name is preserved everywhere except the consumer-facing JSON"name"field. The CodexdisplayNamealso keeps the original for human readability.Type of change
Testing
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)