✨ Publish Git's contextual Apis from @executablemd/git/api - #834
Merged
Merged
Conversation
A contextual Api is a seam: a consumer reaches one to call an operation and replaces one to answer it. Mixed in among records, errors, parsers and component definitions on the package root, those values were indistinguishable from data, and which names were replaceable was something a reader had to already know. `/api` is now that route, and the only one. Eight Apis — Git, RepositoryComposition, RepositoryContext, GitComposition, PullRequestAPI, IssueApi, IssueTrackerContext and GitHost — publish there with their named interfaces, the identity each was minted under, the base refusal each falls back to, the direct operations and accessors consumers call, and every type those interfaces are spelled in. An interface you cannot spell is one you cannot implement, so the types travel with them; they stay on the root too, because a record is still a record when you only want to read one. `IssueTrackerContextApi` is named rather than written inline at its createApi() call. A consumer replacing that context has to spell what it answers with, and the anonymous shape left that as something to copy out of the implementation. The Apis are gone from the root rather than published twice. A seam reachable two ways is two contracts, and the second is whichever the author happened to import. Staying put: the Plugin and its profile predicate, component registrations and definitions, the workflow installation, durable effect identifiers, errors, and record parsers. Two regressions hold it. The portable one imports through the export map under Deno, Node and Bun — the runtimes disagree about which manifest they read, so only running all three tells a missing `deno.json` subpath from a missing `package.json` one — and type-imports the whole contract, since an interface has no runtime presence to check. The source-boundary one judges the two routes by different rules, because they are not the same offence. Importing a record from the root is ordinary, so the root is judged only against the names it no longer publishes. A source module is nobody's route: reaching `../src/issue/api.ts` bypasses the export map entirely, so every name `/api` publishes is a violation there — additive contract types included. Both sets are computed from the two entrypoints rather than listed, so a name added to `/api` is covered the day it is added. That second rule is what the first pass missed: the root could publish no Api while a consumer still reached `GitHostProvider` or `IssueInput` by relative path, which resolves, typechecks, and quietly makes the subpath optional. Twelve such imports existed across nine suites. The root gains @executablemd/git as a development dependency so Workflow's tests resolve the package under Node and Bun. Workflow gains no production dependency and still imports no Git feature.
`RepositoryComposition.operations.ambientRepository()` read as though "ambient" were a kind of repository the caller chose. It is not: the operation answers one question — which Repository is this element acting on when it was written outside a lexical `<Repository>` — and "ambient" describes where the answer came from, not what was asked for. The three answers are unchanged: a selection when the profile has an ambient Repository and this invocation is in one, a throw naming how to run inside one when it has them and this invocation is not, and `undefined` when the profile has no such thing at all. Nothing durable carries the name. It appears in no journal, fixture, snapshot or retained record, so this is a source rename with no compatibility surface. One of the six sites is in a module `grep` calls binary and skips without saying so; the typecheck is what found it.
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.
Follow-up to #831, which merged #822 without this correction.
Two commits, reviewable separately:
b02d7910/api— the reviewed correction83064c5bambientRepository()torepository()#831 shipped
@executablemd/git's first public contract with its contextual Apis on the package root. Architect review identified that as a first-publication defect and this change was reviewed and accepted against it, but it was never pushed before #831 merged — somaincurrently publishes the Apis in the shape the review rejected. This is that correction, unchanged, rebased onto currentmain.Why
A contextual Api is a seam: a consumer reaches one to call an operation and replaces one to answer it.
Mixed in among records, errors, parsers and component definitions on the package root, those values were indistinguishable from data. Which names were replaceable seams was something a reader had to already know.
What changes
Before:
import { Git, RepositoryComposition } from "@executablemd/git"— alongsideGitObjectError,parseWorktreeRecord,gitPluginand eighty other names.After:
import { Git, RepositoryComposition } from "@executablemd/git/api"— and the root no longer publishes them at all.Eight Apis move:
Git,RepositoryComposition,RepositoryContext,GitComposition,PullRequestAPI,IssueApi,IssueTrackerContextandGitHost. Each travels with its named interface, the identity it was minted under, the base refusal it falls back to, the direct operations and accessors consumers call, and every type its interface is spelled in — an interface you cannot spell is one you cannot implement.IssueTrackerContextApiis introduced: that context was the one anonymous exception, written inline at itscreateApi()call, which left the shape a consumer must answer with as something to copy out of the implementation.How it works
The Apis are removed from the root rather than published twice. A seam reachable two ways is two contracts, and the second is whichever the author happened to import. The package had not been published when this was written, so there is no compatibility alias to keep.
Ordinary data types that also appear in an Api signature stay on the root and are re-exported from
/api. A record is still a record when a consumer only wants to read one.Review guide
Start with:
packages/git/api.tsThen review:
packages/git/tests/api-entrypoint.test.ts— the portable contractpackages/git/tests/module-partition.test.ts— the source-boundary scan (bottom of file)packages/git/src/issue/context.ts— the newly namedIssueTrackerContextApiLook carefully at: the source-boundary scan judges the two routes by different rules. That asymmetry is deliberate and is the defect the first pass missed — see below.
What must stay true
/apiand nowhere else — enforced by shape, not by a name list: anything carrying botharoundandoperationsis acreateApi()value, so an Api re-exported from the root fails without anyone maintaining an inventory.packages/git/src, which resolves, typechecks, and quietly makes/apidecorative. The source-boundary scan closes that.@executablemd/gitas a development dependency so Workflow's tests resolve the package under Node and Bun. No production dependency is added.How to verify it
The entrypoint test is portable because the runtimes disagree about which manifest they read. Deno resolves
deno.json; Node and Bun resolvepackage.json. Only running all three distinguishes a subpath missing from one manifest from a subpath missing from the other — and both probes below fail on exactly one side.Every check was run adversarially; all six discriminate:
./apiremoved fromdeno.json./apiremoved frompackage.jsonGitHostomitted from/apiGitHostGitHostre-exported from the rootroot: GitHostapi.tsdropped from the provider-neutral setpackages/git/api.tsGitHostProviderimported from../src/git-host/api.tsThe last one is the defect the first review round caught: the scan originally flagged only names exclusive to
/api, so additive contract types reached by relative path slipped through. Twelve such imports existed across nine suites. The rule is now asymmetric — the root is judged against the names it no longer publishes, a source module against everything/apidoes — and a seventh probe confirms the additive type is still allowed from the root, so the fix did not over-reject.Scope
Included
packages/git/api.tsand./apiin both export mapsIssueTrackerContextApiarchitecture.mdand Git module documentationambientRepository()renamed torepository()(83064c5b). "Ambient" described where the answer came from, not what was asked for, and the Api identity was already…composition.repository. Nothing durable carried the old name — no journal, fixture, snapshot or retained record — so there is no compatibility surface. Its three answers are unchanged.Intentionally unchanged
GIT_HOST_EFFECTandselectedRepositorykeep their source-relative imports; they are not/apicontracts.workspace-effect-loaded-copy.test.tsstays source-relative — the copied module's identity is the subject of that test.PullRequestOperations,IssueOperations,PullRequestReadExecution) stay internal.Generated or mechanical changes
pnpm-lock.yaml(+3) is the link entry for the new root development dependency.deno.lockhas no delta, which is expected for a workspace dependency..github/workflows/publish-packages.ymlhas no delta: a subpath changes neither publication membership nor order.Evidence
All re-run against this base after the rebase.
After the rename (
83064c5b):The rename is worth one note for reviewers. A first sweep reported every site renamed;
deno task checkthen failed inpackages/git/src/deno/composition/provider.ts, which contains a byte that makesgreptreat the file as binary and skip it silently — plaingrepexits 1 wheregrep -afinds the line. The typechecker caught what the search could not. Re-scanned with-a: zero remaining.Scope confirmation