Skip to content

Commit c3bcd57

Browse files
feat(spec-drift): reconcile marketplace-catalog and enforce the last field-level surface
Closes the loop opened in #237: all five field-level surfaces are now ENFORCED freshness gates, and all five are clean. CLEAN agentskills-spec enforced CLEAN claude-hooks enforced CLEAN plugins-reference enforced CLEAN sub-agents enforced CLEAN plugin-marketplaces enforced <- this change projection-freshness: CLEAN (exit 0) over 5 field-level surface(s) WHAT WAS BLOCKING IT Not a kernel fold-in — that framing was wrong and was corrected earlier. What actually blocked it was capture skew: main carried a 2026-06-30 plugin-marketplaces snapshot while the promotion PR (#236) carried 2026-07-23, and the findings were measured against the newer one. Reconciling against the older bytes would have turned an ENFORCED gate red the moment #236 merged — a DAILY red on the watcher, which is precisely how a lane stops being read. #236 has now merged, so the reconciliation lands against the bytes main actually holds. THE FOUR FINDINGS, VERIFIED BEFORE THE RE-VENDOR, NOT ASSUMED 1. catalog.renames — object; former-name to current-name migration map; v2.1.193+ 2. plugin_entry.relevance — object; org-allowlisted suggestion signals; v2.1.152+ 3. relevance joins the documented marketplace_specific_fields list 4. catalog.reserved_names gains `first-party-plugins` and `healthcare` Each has its own table row AND its own doc section on the captured page, and is absent from the frozen one. (4) IS THE MATERIAL ONE AND IS NOT BOOKKEEPING The page now states Claude Code re-checks reserved names on EVERY marketplace load, not only on add — so a marketplace already registered under a newly-reserved name STOPS LOADING and reports an untrusted source. Before v2.1.205 it kept loading. That is a live behavioural change to how a marketplace can fail. Estate exposure was checked, not assumed: no marketplace.json under the projects tree uses either new name. The IS marketplace is `claude-code-plugins-plus`, which is not reserved — but it IS a suffix-extension of the reserved `claude-code-plugins`, and the same paragraph also blocks names that "impersonate official marketplaces" by prose judgement. Recorded as a watch item, not a break. This finding is the clearest argument yet for the whole track: it is a real, dated, operational risk to our own marketplace that surfaced only because a gate could finally see current bytes. NO KERNEL EDIT IMPLIED upstream-base/marketplace-catalog.v1.json is a required-set FLOOR: it models required:[name, owner, plugins] plus the constraints its negative corpus tests, enumerates no optional plugin-entry fields at all, and leaves additionalProperties permissive. Both new fields are optional, so they already validate; adding them would make the schema LOOK current while remaining a floor. PINNED EXPECTATIONS MOVED, WHICH IS THE MECHANISM WORKING The extractor's self-test failed loudly on four hard-coded expectations (8->9 top-level fields, 14->16 reserved names, 20->21 entry fields with standard-metadata 12->13, and `renames`/`relevance` joining the doc-fields-not-in-kernel divergences). Those sets exist precisely so a re-capture that shifts a finding cannot pass silently and a human must reconcile. Updated them as that human, with the reasoning recorded in vendor-meta and the registry note rather than as bare number edits. A TEST RETIRED HONESTLY test_undispositioned_contracts_still_report_their_known_drift asserted marketplace-catalog still reported DRIFT, and was written to fail if that went green WITHOUT the registry flipping to `failing`. The registry flipped, so the premise retired. Chose to fold the contract into test_reconciled_contracts_are_clean and document the retirement in place, rather than delete the test to make a suite pass. The anti-vacuity property it guarded is unchanged: test_check_fresh_fires_on_a_perturbed_capture still proves the check fires on real captured bytes per contract, and test_the_real_coverage_map_runs_and_agrees_with_the_registry still requires every `failing` surface to actually be clean. VERIFICATION - All five extractors --check + --self-test OK, including the reconciled kernel cross-check whose agreements AND divergences now match their updated sets exactly. - projection-freshness exit 0, 5/5 enforced CLEAN. - pytest scripts/tests/ -q: 226 passed. - check-surface-registry OK; check-vendor-meta-integrity OK (5 captures, 23 files, every sha256 + byte count matching); detector-health and sak-dashboard --check OK; audit-harness verify OK after init. Refs #236, #237, #239
1 parent fe393b7 commit c3bcd57

7 files changed

Lines changed: 234 additions & 124 deletions

File tree

scripts/extract-marketplace-catalog-projection.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -670,13 +670,20 @@ def cmd_check_fresh(vendor_dir: str, surface: str | None = None) -> int:
670670
"plugin-entry-minimum-name-plus-source-both-sides",
671671
"samples-corroborate:top-level-keys-entry-fields-and-source-types-within-documented-sets",
672672
]
673+
# Updated 2026-07-23 with the re-vendor to the 2026-07-23 capture. The shifts are
674+
# the reconciliation itself, not a loosening: `renames` and `relevance` join the
675+
# doc-fields-not-in-kernel lists (both OPTIONAL, and the kernel's upstream-base is a
676+
# required-set FLOOR that models no optional entry fields at all, so no authoring/v1
677+
# edit is implied), and the reserved-name count moves 14 -> 16. These sets are pinned
678+
# precisely so a re-capture that shifts a finding fails LOUD and a human reconciles —
679+
# which is exactly what happened here.
673680
EXPECTED_DIVERGENCES = [
674681
"plugins-min-items:kernel-requires-minItems-1;doc-states-no-minimum",
675-
"doc-top-level-optional-fields-not-in-kernel:$schema,allowCrossMarketplaceDependenciesOn,description,version",
682+
"doc-top-level-optional-fields-not-in-kernel:$schema,allowCrossMarketplaceDependenciesOn,description,renames,version",
676683
"doc-plugin-entry-optional-fields-not-in-kernel:agents,author,category,commands,defaultEnabled,description,"
677-
"displayName,homepage,hooks,keywords,license,lspServers,mcpServers,repository,skills,strict,tags,version",
684+
"displayName,homepage,hooks,keywords,license,lspServers,mcpServers,relevance,repository,skills,strict,tags,version",
678685
"source-forms:doc-documents-relative-path-string-plus-4-object-types;kernel-leaves-source-unmodeled",
679-
"name-constraints:kernel-adds-maxLength-64-and-kebab-regex;doc-prose-kebab-case-plus-14-reserved-names-not-encoded",
686+
"name-constraints:kernel-adds-maxLength-64-and-kebab-regex;doc-prose-kebab-case-plus-16-reserved-names-not-encoded",
680687
"samples:tolerances-outside-documented-surface:github:commit,url:path,plugin-name:wordpress.com",
681688
]
682689

@@ -1055,13 +1062,13 @@ def check(label: str, condition: bool) -> None:
10551062
top = projection["catalog"]["top_level_fields"]
10561063
entry_fields = projection["plugin_entry"]["fields"]
10571064
samples = projection["samples"]
1058-
check("real capture: 8 documented top-level fields (3 required)", len(top) == 8 and len([f for f, e in top.items() if e["required"]]) == 3)
1059-
check("real capture: 14 reserved marketplace names", len(projection["catalog"]["reserved_names"]) == 14)
1065+
check("real capture: 9 documented top-level fields (3 required)", len(top) == 9 and len([f for f, e in top.items() if e["required"]]) == 3)
1066+
check("real capture: 16 reserved marketplace names", len(projection["catalog"]["reserved_names"]) == 16)
10601067
check(
1061-
"real capture: 20 documented plugin-entry fields (2 required, 12 standard-metadata, 6 component-config)",
1062-
len(entry_fields) == 20
1068+
"real capture: 21 documented plugin-entry fields (2 required, 13 standard-metadata, 6 component-config)",
1069+
len(entry_fields) == 21
10631070
and len([f for f, e in entry_fields.items() if e["required"]]) == 2
1064-
and len([f for f, e in entry_fields.items() if e["scope"] == "standard-metadata"]) == 12
1071+
and len([f for f, e in entry_fields.items() if e["scope"] == "standard-metadata"]) == 13
10651072
and len([f for f, e in entry_fields.items() if e["scope"] == "component-config"]) == 6,
10661073
)
10671074
check(

scripts/tests/test_extractor_freshness.py

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -137,23 +137,23 @@ def test_samples_are_never_repointed(extractors: dict[str, ModuleType], contract
137137
# ── Can it still fire? (a green result must mean agreement, not an unreachable check) ──
138138

139139

140-
@pytest.mark.parametrize("contract", ["hook-config", "plugin-manifest", "agent-definition"])
140+
@pytest.mark.parametrize("contract", CONTRACTS)
141141
def test_reconciled_contracts_are_clean(extractors: dict[str, ModuleType], contract: str) -> None:
142-
"""The control. These three were reconciled against the current capture."""
143-
assert extractors[contract].cmd_check_fresh(_vendor_dir(contract)) == CLEAN
144-
145-
146-
@pytest.mark.parametrize("contract", ["marketplace-catalog"])
147-
def test_undispositioned_contracts_still_report_their_known_drift(
148-
extractors: dict[str, ModuleType], contract: str
149-
) -> None:
150-
"""Proves --check-fresh is not vacuous on REAL captured content.
151-
152-
marketplace-catalog carries findings recorded in the registry as awaiting a
153-
kernel fold-in. If this ever goes green without the registry flipping to
154-
`failing`, either the finding was silently reconciled or the check went blind.
142+
"""The control. All four are now reconciled against the current capture.
143+
144+
RETIRED alongside this: a companion test asserting marketplace-catalog still
145+
reported DRIFT. Its premise was "this contract carries findings awaiting
146+
disposition", and it was written to fail if that ever went green WITHOUT the
147+
registry flipping to `failing` — because that would mean the finding was
148+
silently reconciled or the check went blind. The registry did flip, so the
149+
premise retired honestly rather than being deleted to make a suite pass.
150+
151+
The anti-vacuity property it guarded is not lost: it lives in
152+
test_check_fresh_fires_on_a_perturbed_capture (per contract, on real captured
153+
bytes) and in test_the_real_coverage_map_runs_and_agrees_with_the_registry
154+
(every surface the registry marks `failing` must actually be clean).
155155
"""
156-
assert extractors[contract].cmd_check_fresh(_vendor_dir(contract)) == DRIFT
156+
assert extractors[contract].cmd_check_fresh(_vendor_dir(contract)) == CLEAN
157157

158158

159159
# ── Open-clause value recording (the permissionMode class of loss) ────────────

0 commit comments

Comments
 (0)