You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
public-api.api.txt records the signatures of the promised tier. Nothing checks that a type named in one of those signatures is reachable from api.
So a change can widen a promised function with a parameter type it never re-exports, the snapshot records the new signature happily, public-api goes green, and the promised surface is left naming a type a consumer of api cannot spell.
Two instances, one caught by a reviewer and one by an unrelated PR's test. Nothing in CI saw either.
Why the existing guards do not cover it
public-api.api.txt is a record of what the surface is, regenerated and diffed by the public-api job. It is doing its job correctly: the signature really did change, and the snapshot really does say so. The question it never asks is whether the names inside a signature are reachable by the same route as the function.
The fold's own test in #520 (api_removal_is_self_sufficient.rs) catches it for one function because it is written to use api paths exclusively. That is a guard for one call site, held in place by a convention that is not stated anywhere and that the next promoted function will not inherit.
Proposed shape
A test over public-api.api.txt itself: for every devlaunch_core::api:: row, extract the fully qualified type paths it names, and assert each one either
is itself a devlaunch_core::api:: row, or
appears in the file as re-exported from api,
with the primitives and core::/alloc::/std:: paths excluded. Failure names the row and the unreachable type.
It reads a file that is already generated, so it is not a second hand-maintained copy of anything, and it costs nothing at runtime.
Decide deliberately whether a type reachable via a public module path that api does not re-export counts as satisfied. KeptCopies was reachable at devlaunch_core::flows::kept_copies::KeptCopies the whole time; what it was not, was reachable through api. If the promise is "api is self-sufficient", public-elsewhere is not enough, and the test should say so in its name.
Related to the standing rule in AGENTS.md about second copies: this is the inverse case, where the generated record is correct and the reachability it implies is not checked at all.
Related: #517, #527, #529 — signals whose green means less than a reader assumes.
The gap
public-api.api.txtrecords the signatures of the promised tier. Nothing checks that a type named in one of those signatures is reachable fromapi.So a change can widen a promised function with a parameter type it never re-exports, the snapshot records the new signature happily,
public-apigoes green, and the promised surface is left naming a type a consumer ofapicannot spell.It has happened twice
&devlaunch_core::flows::kept_copies::KeptCopiesas a sixth parameter toapi::workspace_deleteand re-exported nothing. It merged. It was found only when Fold the removal guard into one workspace_remove and swap the api row #520 folded that function and its own test, which deliberately imports throughapiand nothing else, failed to compile.Two instances, one caught by a reviewer and one by an unrelated PR's test. Nothing in CI saw either.
Why the existing guards do not cover it
public-api.api.txtis a record of what the surface is, regenerated and diffed by thepublic-apijob. It is doing its job correctly: the signature really did change, and the snapshot really does say so. The question it never asks is whether the names inside a signature are reachable by the same route as the function.The fold's own test in #520 (
api_removal_is_self_sufficient.rs) catches it for one function because it is written to useapipaths exclusively. That is a guard for one call site, held in place by a convention that is not stated anywhere and that the next promoted function will not inherit.Proposed shape
A test over
public-api.api.txtitself: for everydevlaunch_core::api::row, extract the fully qualified type paths it names, and assert each one eitherdevlaunch_core::api::row, orapi,with the primitives and
core::/alloc::/std::paths excluded. Failure names the row and the unreachable type.It reads a file that is already generated, so it is not a second hand-maintained copy of anything, and it costs nothing at runtime.
Notes for whoever builds it
apire-export ofKeptCopiesand the guard should nameapi::workspace_removeandKeptCopies. Verify it red that way rather than on a synthetic row.apidoes not re-export counts as satisfied.KeptCopieswas reachable atdevlaunch_core::flows::kept_copies::KeptCopiesthe whole time; what it was not, was reachable throughapi. If the promise is "apiis self-sufficient", public-elsewhere is not enough, and the test should say so in its name.Related: #517, #527, #529 — signals whose green means less than a reader assumes.