Skip to content

feat(config): publish the config contract at its registry key - #75

Merged
fx merged 3 commits into
mainfrom
feat/publish-config-contract
Sep 6, 2026
Merged

feat(config): publish the config contract at its registry key#75
fx merged 3 commits into
mainfrom
feat/publish-config-contract

Conversation

@fx

@fx fx commented Sep 6, 2026

Copy link
Copy Markdown
Owner

Publishes the config capability's contract at @fx/tx/config over 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. The dialogs key 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 Config and ConfigValidator file-locally in plugins/config/index.ts and exported neither, so the only exported copy in the repository lived in plugins/marketplace/configured.ts — a module that imports the marketplace manager, some two thousand lines reaching node:child_process, node:fs, and the marketplace's own source and storage modules. The type itself needs none of it.

plugins/marketplace/configured.ts is now a consumer of the published contract rather than the place the type is declared. requireConfigCapability stays 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 check passes — 1142 tests, 0 failures, 100% function and line coverage across production sources, build succeeds.
  • The change document's scenario — the config contract carries no implementation — is proved directly. A new boundary test resolves @fx/tx/config through the exports map to its types target 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 asserts plugins/marketplace/manager.ts, source.ts, and storage.ts are absent — each first checked to exist on disk, so the absence assertions cannot pass by naming files that were never there.
  • That closure helper is proved non-vacuous. Run against @fx/tx/grid it returns ["plugins/grid/contract.ts", "plugins/theme/contract.ts"], correctly following grid's cross-contract @fx/tx/theme import. Against @fx/tx/config it returns the contract alone.
  • Second, independent proof of the same property: test/plugin-consumer.test.ts packs the tarball, installs it, copies plugins/marketplace into the fixture, and type checks it against the installed package — whose files list carries no marketplace module at all. That the import type checks there is itself evidence the contract needs none of them.
  • test/plugins.test.ts derives its cases from the exports map, so import("@fx/tx/config") is asserted to reject at run time with no edit needed.
  • The exact packed-file assertion and the consumer fixture both cover the new subpath; the fixture declares a guard typed by the contract and reads 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

fx added 3 commits September 6, 2026 04:58
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
Copilot AI lite review requested due to automatic review settings September 6, 2026 05:04

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟢 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.ts and publish it via package.json exports as @fx/tx/config.
  • Rename the config registry key from config to @fx/tx/config across 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.

@fx
fx merged commit dea5355 into main Sep 6, 2026
2 checks passed
@fx
fx deleted the feat/publish-config-contract branch September 6, 2026 05:07
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.

2 participants