feat(config): publish the config contract at its registry key - #75
Merged
Conversation
Publish the config capability's structural contract over the mechanism the theming and grid contracts established. `plugins/config/contract.ts` declares `Config` and `ConfigValidator` as types alone and `package.json` publishes them at `./config` under a `types` condition with no runtime condition beside it, so the capability's value keeps coming from the registry alone. Config is the case that shows what publication removes rather than adds. The provider declared the shape file-locally and exported neither half of it, so the only exported copy in the repository sat in `plugins/marketplace/configured.ts` — a module that imports the marketplace manager and, behind it, `node:child_process`, `node:fs`, and the marketplace's source and storage modules. Naming what a persisted value looks like needs none of that. That file now imports the contract and keeps what is its own: the marketplace's config key, the value it stores under it, and `requireConfigCapability`, which is a lookup with a consumer's policy in it rather than part of the contract. The registry key becomes that same specifier in the same commit. A key that named a specifier publishing nothing would be a tx violating the rule its own specification states, so `config` is renamed at the provider, at the marketplace's lookup, and in the tests naming it. This is a breaking change to anything reading the capability and is accepted as one: no alias and no dual registration, because registering under both spellings would put two entries in one snapshot. The plugin's own identity name stays bare — that names the plugin, not the key. The boundary suite gains the graph the change document asks it to prove. Resolving a published specifier is walked to its transitive closure, following both a relative path beside a contract and another published specifier, and the config contract's closure is the contract alone — with the manager, the source resolver, and the install storage named and checked to exist, so their absence cannot pass by naming files that are not there. Claude-Session: https://claude.ai/code/session_0165ZoEriyA7L7iLAG81AkR3
The guide's config section told a consumer to declare the capability's shape locally and called the shape an implementation detail rather than a public export. It is now published at the key it is read from, so the section shows the import instead, describes what the contract carries rather than restating it in a block that could drift from the declarations it copies, and states the exactly-one rule the marketplace's lookup already applies. The dialogs section is untouched: that contract is not published yet, and the rest of change 0031 rewrites it when it is. The published-contract lists in the guide, in `AGENTS.md`, and in `REVIEW.md` each name `@fx/tx/config` beside the three that were already there. The config specification's status note said the contract was not published and the key still bare; both are now false, so it records the half of change 0031 that has landed and says the change stays open for dialogs. The plugin-system note said change 0031 had not started and that both keys remained bare, and now distinguishes the two. The dialogs specification's note is untouched, because nothing in it has stopped being true. Change 0031's first task group is checked off. Its status and its documentation index entry stay draft: the dialogs contract is still unpublished, and only the change's final implementing commit flips them. Claude-Session: https://claude.ai/code/session_0165ZoEriyA7L7iLAG81AkR3
The guide's rewritten config section told every consumer it must find exactly one provider, which is the marketplace plugin's own choice rather than a rule the project makes. The config specification leaves behavior with additional values under the key to neither itself nor the initial consumer contract, and the plugin-system specification's own scenario says a published contract neither supplies a value, selects among values, nor rejects one — so a reader following the section would have been told to enforce something the contract must not. The section now says what the contract does and does not decide, names the strict policy as the marketplace's with the reason it chose it, and says a consumer with a reason to tolerate an absent provider may choose otherwise. The theming section's parallel clause named config as already obliged to the same rule and is corrected with it, so the two sections no longer disagree about who owns the decision. What theming requires of its own consumers is unchanged. Claude-Session: https://claude.ai/code/session_0165ZoEriyA7L7iLAG81AkR3
There was a problem hiding this comment.
🟢 Approval recommended
The key rename and contract publication are consistent across code/docs/tests, with explicit boundary tests asserting the contract’s import closure stays implementation-free.
Pull request overview
Publishes the config capability’s type-only contract at the same registry key it is registered under (@fx/tx/config), then migrates bundled consumers/tests/docs away from local structural copies so the contract can be imported without dragging marketplace implementation types along.
Changes:
- Add
plugins/config/contract.tsand publish it viapackage.jsonexportsas@fx/tx/config. - Rename the config registry key from
configto@fx/tx/configacross provider/consumer/test usage. - Add/adjust boundary + consumer fixture tests and update specs/guides/task tracking to reflect the publication and key rename.
File summaries
| File | Description |
|---|---|
| test/plugin-consumer.test.ts | Ensures the packed artifact includes the config contract and a consumer can type/read it via @fx/tx/config. |
| test/plugin-boundary.test.ts | Adds a closure test proving @fx/tx/config compiles only the contract module (no implementation reachability). |
| test/marketplace-plugin.test.ts | Updates the marketplace test double to implement/import the published Config contract and registers it under @fx/tx/config. |
| test/configured-marketplaces.test.ts | Switches Config typing to import from @fx/tx/config. |
| test/config-plugin.test.ts | Types the provider tests against the published contract and updates the registrations key to @fx/tx/config. |
| REVIEW.md | Updates plugin-boundary checklist to include the newly published @fx/tx/config contract. |
| plugins/marketplace/configured.ts | Stops exporting Config/ConfigValidator and instead imports Config from @fx/tx/config; updates lookup to @fx/tx/config. |
| plugins/config/index.ts | Providers now type-check against the adjacent contract and register under @fx/tx/config. |
| plugins/config/contract.ts | New published type-only contract defining Config and ConfigValidator. |
| package.json | Adds ./config exports.types and includes the contract file in files. |
| docs/specs/plugin-system/index.md | Updates “Published Capability Contracts” status to reflect config now published and dialogs still pending. |
| docs/specs/config/index.md | Updates status language to reflect the config publication/key rename and consumer migrations as current behavior. |
| docs/manual/plugins.md | Updates the config section to import from @fx/tx/config, use the new key, and clarifies that selection policy is consumer-owned. |
| docs/changes/0031-publish-the-dialogs-and-config-contracts.md | Marks the config task group complete while leaving dialogs work pending. |
| AGENTS.md | Updates plugin guidance to include type-only imports from @fx/tx/config among published contracts. |
Review details
- Files reviewed: 15/15 changed files
- Comments generated: 0
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
6 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Publishes the config capability's contract at
@fx/tx/configover the mechanism change 0030 established, and retires the copies its consumers maintained.This is the first of two PRs implementing docs/changes/0031-publish-the-dialogs-and-config-contracts.md — its first task group only. That document stays
**Status:** draft; the final PR, which publishes the dialogs contract, flips it. Thedialogskey deliberately stays bare until then, because a key must never name a specifier that publishes nothing.Why config first
Config is the sharper demonstration that publication removes a real coupling rather than adding a file. The provider declared
ConfigandConfigValidatorfile-locally inplugins/config/index.tsand exported neither, so the only exported copy in the repository lived inplugins/marketplace/configured.ts— a module that imports the marketplace manager, some two thousand lines reachingnode:child_process,node:fs, and the marketplace's own source and storage modules. The type itself needs none of it.plugins/marketplace/configured.tsis now a consumer of the published contract rather than the place the type is declared.requireConfigCapabilitystays exactly where it is — it is a lookup with a consumer-owned policy baked in, not a contract, and publishing it would hand every consumer one answer to a question the specification says they own.Test plan
bun run checkpasses — 1142 tests, 0 failures, 100% function and line coverage across production sources, build succeeds.@fx/tx/configthrough theexportsmap to itstypestarget and walks the transitive import closure using the TypeScript program's own specifier list, so type-only imports are followed rather than erased the way a transpiler scan would erase them. It asserts the closure is exactly["plugins/config/contract.ts"], and separately assertsplugins/marketplace/manager.ts,source.ts, andstorage.tsare absent — each first checked to exist on disk, so the absence assertions cannot pass by naming files that were never there.@fx/tx/gridit returns["plugins/grid/contract.ts", "plugins/theme/contract.ts"], correctly following grid's cross-contract@fx/tx/themeimport. Against@fx/tx/configit returns the contract alone.test/plugin-consumer.test.tspacks the tarball, installs it, copiesplugins/marketplaceinto the fixture, and type checks it against the installed package — whosefileslist carries no marketplace module at all. That the import type checks there is itself evidence the contract needs none of them.test/plugins.test.tsderives its cases from theexportsmap, soimport("@fx/tx/config")is asserted to reject at run time with no edit needed.registrations<Config>("@fx/tx/config").A spec violation caught pre-PR
The local Codex review found one blocking issue, and it was correct. The rewritten config section of the plugin guide told every consumer it "must find exactly one" provider. But docs/specs/config/index.md § Registry Use leaves behaviour with additional values under the key unspecified, and the plugin-system spec's "Publication leaves the registry alone" scenario requires that a published contract "neither supplies a value, selects among values, nor rejects one". The guide was instructing readers to enforce a policy the contract must not carry.
Fixed as a class rather than an instance: the section now states what the contract does and does not decide, names the strict one-provider rule as the marketplace's own policy with its reason, and says a consumer may choose otherwise. The theming section carried a parallel clause ("as a config consumer already must") asserting the same false obligation, and was corrected in the same commit so the two sections no longer disagree. What theming requires of its own consumers is unchanged.
Spec: docs/specs/config/, docs/specs/plugin-system/index.md § Published Capability Contracts
Change: docs/changes/0031-publish-the-dialogs-and-config-contracts.md (first task group)
https://claude.ai/code/session_0165ZoEriyA7L7iLAG81AkR3