diff --git a/docs/roadmap/remediation.md b/docs/roadmap/remediation.md index 157ef0a..8602389 100644 --- a/docs/roadmap/remediation.md +++ b/docs/roadmap/remediation.md @@ -146,3 +146,63 @@ standalone installation step. Use the existing recipe where a copyable command is useful. Keep the selected version, install location, and `go run` exit-status caveat. An already installed native binary does not need a new bootstrap step. + +## Algorithmic audit + +The host normalization itself adds a fixed number of Go invocations per +launch. The CLI renders a fixed argument list, so quoting and formatting take +time and space linear in the lengths of the directory and record paths. Go +dependency inspection and compilation retain their existing roles. The +findings below concern a pre-existing algorithm on the audited recovery path +and a new module-selection defect in the smoke recipe. + +### Relocate recovery replacements in one pass without merging selectors + +**Evidence:** The API-only path in +[`RecoveryTool`](../../internal/gotool/recovery.go) walks the parsed +`go.mod` replacements. It calls `modfile.AddReplace` for each relative +local replacement. In the pinned `golang.org/x/mod` implementation, +each call scans the entire replacement slice. With `R` total directives +and `L` relative local directives, relocation visits `O(R × L)` entries, +and takes quadratic time when all or most entries require relocation. +This loop predates host normalization and still runs before the new host +query. + +The same loop can panic on a valid module that declares both an unversioned +and a version-specific replacement for the same dependency. When the +unversioned directive appears first and uses a relative path, calling +`AddReplace` with an empty old version treats both directives as matches +and clears the version-specific entry. The outer loop then visits that +cleared entry as if it were another relative replacement. In a disposable +API-only consumer with both forms for ReleaseKit, Go accepted the module +and validation passed. After a local hook rejected the tag push, ReleaseKit +saved the publication record, then panicked in `modfile.addReplace`. It +exited with status 2 instead of reporting the rejected push and a recovery +handoff. + +**Fix:** Rewrite each relative right-hand path on its existing parsed +directive in one pass, preserving the exact left-hand path and version +selector. For example, update the typed replacement and its corresponding +syntax token before formatting. This takes `O(R)` directive visits plus +the bytes processed for the paths and avoids selector merging. Keep the +existing relocation policy, the separate synthetic replacement for the +original module, and the private module lifetime. Add a recovery fixture +with distinct local destinations for a wildcard and a version-specific +replacement. Check that the handoff preserves both directives, selects the +version-specific destination, and reports the original publication status. + +### Anchor smoke-check module paths to the intended checkout + +**Evidence:** The [multi-module smoke recipe](../../examples/multi-module/README.md) +replaced `go -C integrations/...` with subshells that run +`cd integrations/...` before host discovery. A shell with `CDPATH` set can +resolve these relative names in another checkout. In a disposable probe +with matching module paths under two directories, the published snippet +sent both nested checks to the checkout named by `CDPATH` and returned +success. If the other checkout has valid profiles and tools, the smoke +result can appear valid for the wrong candidate. + +**Fix:** Use `cd ./integrations/ckconfig` and `cd ./integrations/core/v2`, +or clear `CDPATH` for those two directory changes. The number of lookups is +already fixed; the change corrects which directory is selected. Verify the +copyable recipe with a conflicting `CDPATH` entry.