Repository navigation
fix(generate): --services narrows the whole library, not just the facade - #1179
Merged
antoinetoussaint-byte merged 4 commits intoOct 26, 2026
Merged
Conversation
`generate client --services` restricted only the facade: the persisted descriptor set, the catalog, the manifest's sources[].services and every language's bindings still carried every service of the contract (#1173). The library is now narrowed before anything is generated. The contract keeps the named services and the declarations they reach (their methods' messages and, transitively, every message and enum those name); a file defining a custom option they set is kept whole, as is a shared upstream file; no other service is declared anywhere. Catalog, manifest, bindings and facade all come from the narrowed contract, and its digest is the one recorded. A name the contract does not declare is refused by name. An aggregated library (sync solution-sdk) is narrowed as one, so a file two entries carry comes out the same for both. The docs' "--services does not work over a descriptor set" limitation is gone: the facade plugins run with strategy: all, and the qualified tests generate a narrowed descriptor set in Go, TypeScript and Python. Refs #1173 (the narrowing; the issue's --check mode is not part of this change) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ervices-narrows-all
…ervices-narrows-all
…rrowing follows extensions (r140) Review r140 of #1179: - F1/F7: the entry keeps the producer's contract digest. It is what sources[].contract-digest and clients.codefly.json both record, so editing an entry's services or adding an entry regenerates without --force; only a new producer contract needs it. The catalog under contract/ carries the digest of the contract persisted beside it. sync solution-sdk gains the --force its refusal names. - F2: each entry persists its own closure; bindings are generated from the library-wide closure, so a shared file is still generated once. - F3: options on extension ranges are visited. - F4: every extension of a kept message is kept, wherever it is declared. - F5: two entries carrying different versions of one file are refused, naming the file and both entries. - F6: extensions set inside message-valued options are followed, through the contract's descriptors or the protobuf runtime's. Refs #1173 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merged
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.
What was wrong
codefly generate client --services <S>narrowed only the facade. The library still carried every service of the contract (#1173):contract/contract.binpb;contract/catalog.codefly.json;library.codefly.yaml'ssources[].services;So a library generated for one saas service carried all 36 of them. That blocks module-saas-starter#1217 (B2).
What changes
GenerateClientLibrarynarrows its entries before anything is generated (pkg/generators/narrow.go,narrowEntries).What the persisted contract keeps:
Files kept whole:
google/protobuf,google/api,google/rpc,google/type,buf/validate), in its canonical shape.How the result is built:
protodesc.NewFiles) before it is used.Digests. The digest that identifies a library's source stays the producer's contract digest:
sources[].contract-digestandclients.codefly.jsonrecord that same digest.--force.contract/catalog.codefly.jsoncarries the digest of the narrowedcontract.binpbwritten beside it.--force.Outputs:
sources[].services, the bindings and the facade in Go, TypeScript and Python all come from the narrowed contract.--from module/servicewith--servicesnow generates from the narrowed descriptor set, because the proto sources declare every service.Aggregated libraries (
sync solution-sdk):sync solution-sdkgains the--forceflag that its digest refusal tells you to use.Reuse
checkContractDiamond, which still runs on the producer contracts;markUpstreamModulesAsImports,targetFilesForPackage);upstreamProtoModulestable, through a language-independentmatchUpstreamModulethatupstreamModuleFornow calls.Why not buf's
--typefiltering. The companion's buf (1.73.0) hasbuf build <image> --type <name>, which filters one image down to the closure of the named types. I did not use it, for three reasons:--typeruns would cut a shared file differently for each entry, and the merged tree would keep whichever copy was written last. That is the conflictnarrowEntriesprevents.pkg/generatorshas no plumbing for running buf in the companion; using it would mean adding a second Docker path.Size.
narrow.gois about 720 lines including comments and doc blocks, over the ~500-line guideline. Review r140's F3–F6 added the extension and nested-option handling.Classification
Fix, made in the generator that owns what a library contains.
docs/commands.mdsaid--servicesdoes not work over a descriptor set. That limitation is gone, because the facade plugins now run withstrategy: all, so the paragraph now describes what--servicesdoes.docs/cli-reference.mdwas regenerated.Tests
pkg/generators/narrow_test.go(no Docker):cmd/generate/client_services_test.go(Docker-gated):TestGenerateClientServicesNarrowsTheWholeLibrary: generating one service of a real buf-compiled two-service contract in Go, TypeScript and Python carries only that service and what it needs. The manifest records the producer digest.go vetand a Python import both pass. This test fails without the narrowing.TestGenerateClientServicesIsStable: regenerating in place, and generating afresh into another directory, both reproduce the library byte for byte.TestGenerateClientServicesSelectionChangesRegenerateWithoutForce: editing an entry's services, then adding a second entry, both regenerate without--force. Each entry's persisted contract declares only its own service. Both record the producer digest. Regenerating is stable and the bindings import. With the old narrowed-digest behaviour, this test fails with the refusal. It uses Python because one Go package cannot hold two facades of different names; that limit of aggregated Go libraries predates this change.Evidence
CODEFLY_GENERATE_QUALIFY=1 go test -tags proto_companion_required ./cmd/generate/ ./cmd/publish/ ./cmd/sync/ ./pkg/generators/ -count=1passed locally on proto companion 0.0.18 (ghcr.io/codefly-dev/proto:0.0.18).go test -race ./pkg/generators/ ./cmd/sync/passed.golangci-lintreports 0 issues on the changed packages.npm install && npm run build) at the first head. It built cleanly and mentioned no other service.Not in this PR
The issue also asks for a
--checkmode ongenerate client. That flag is not here; the drift test above is the behaviour such a flag would gate on.sync solution-sdk --checkalready exists.Refs #1173
🤖 Generated with Claude Code