fix(vendor-graph): track-minor was track-latest — follow the pinned x.y line - #44
Open
mdheller wants to merge 1 commit into
Open
fix(vendor-graph): track-minor was track-latest — follow the pinned x.y line#44mdheller wants to merge 1 commit into
track-minor was track-latest — follow the pinned x.y line#44mdheller wants to merge 1 commit into
Conversation
….y line
`track-minor` was implemented with `majorOf`:
const sameMajorNewer = intervening.some((a) => majorOf(a.version) === majorOf(pinnedVersion))
const majorMoved = releaseDistance > 0 && majorOf(latestVersion) !== majorOf(pinnedVersion)
freshnessState = sameMajorNewer || majorMoved ? 'stale' : 'current'
A policy whose NAME states one contract and whose CODE honours another. And it is
worse than "track-minor is really track-major": the two arms are EXHAUSTIVE — if
nothing newer shares the pinned major, the newest release cannot either, so
`majorMoved` fires. `releaseDistance > 0` was therefore always stale, which is
`track-latest`. Three declared policies, two distinct behaviours, and the collapsed
one is the DEFAULT that every pin in the live vendor register declares.
Consequence, measured before the change: a pin at 0.4.46 was reported stale against
0.5.0, and `proposeRevendor` sealed an EffectRequest to take it. A manifest author
who wrote `track-minor` to stay on a minor line was silently opted in to leaving it.
Established semantics — a pin on `x.y.*` follows its own minor line:
· 0.4.46 → 0.4.47 in-lane ⇒ stale, target 0.4.47 (accepted)
· 0.4.46 → 0.5.0 off-lane ⇒ current (refused)
· 0.4.46 → 1.0.0 off-lane ⇒ current (refused)
`current` here never means "nothing happened upstream": the rationale names every
off-line release and `releaseDistance`/`latestVersion` still report the real gap.
Also fixes the same defect one layer down. `proposeRevendor` took
`verdict.latestVersion`, so a correctly-stale track-minor pin at 0.4.46 with 0.4.47
AND 0.5.0 upstream would have proposed → 0.5.0: the exact bump the policy refuses,
under a rationale saying it was withheld. `StalenessVerdict` now carries
`policyTargetVersion` — the newest release the POLICY will take — and the proposal
uses it. track-latest is unchanged and still proposes the newest release.
Two boundary rules, stated so they are not rediscovered as bugs:
· a pin that is not release-shaped (a sha, `unknown`) names no line, so its state
is `unknown` — the same answer, for the same reason, that an unobserved pin gets
· a prerelease does not make a stable pin stale (semver ranges treat prereleases as
opt-in); a pin that is itself a prerelease does follow them
The line is derived through this branch's `parseVersion`, not `split('.')`, so
`v0.4.46`, `0.4.46+build.7` and `0.4` all resolve to the 0.4 line — which is why this
stacks on #42 rather than duplicating a version parser.
Proof: the 9 new tests fail against the `majorOf` implementation and pass against
this one. 426/426 green, typecheck clean, ts/dist rebuilt.
There was a problem hiding this comment.
Pull request overview
Note
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Fixes track-minor freshness semantics so it follows the pinned x.y.* minor line (not “latest”), and ensures revendor proposals target the version allowed by the policy.
Changes:
- Add
policyTargetVersionto staleness verdicts and use it forproposeRevendor. - Rework
track-minorlogic to compute staleness based on the pinned minor line and prerelease rules. - Add targeted tests covering minor-line behavior, prerelease handling, non-release-shaped pins, and proposal targeting.
Reviewed changes
Copilot reviewed 2 out of 6 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| ts/src/vendor-graph.ts | Implements minor-line tracking, adds policyTargetVersion, and updates proposal targeting logic. |
| ts/src/vendor-graph.test.ts | Adds regression tests validating the corrected track-minor semantics and proposal behavior. |
Comments suppressed due to low confidence (1)
ts/src/vendor-graph.ts:1
StalenessVerdictis exported, and adding a new required field is a breaking change for downstream TypeScript consumers that implement/construct this interface or rely on structural typing. If this package is intended to publish a patch-level fix, consider makingpolicyTargetVersionoptional with a well-defined fallback, or ensure the release/versioning strategy reflects this breaking API change.
/**
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+878
to
+881
| const newestOnLine = onLine[onLine.length - 1]?.version | ||
| // The proposal target is the newest IN-LANE release, never `latestVersion`. `0.5.0` being | ||
| // newer is not a reason to propose it to a pin that follows `0.4.*`. | ||
| if (newestOnLine !== undefined) policyTargetVersion = newestOnLine |
Comment on lines
+772
to
+776
| function minorLineOf(v: string): string | undefined { | ||
| const p = parseVersion(v) | ||
| if (!p) return undefined | ||
| return `${p.release[0] ?? '0'}.${p.release[1] ?? '0'}` | ||
| } |
Comment on lines
+871
to
+876
| const followsPre = isPrerelease(pinnedVersion) | ||
| const onLine = intervening.filter((a) => | ||
| minorLineOf(a.version) === line && | ||
| compareVersions(a.version, pinnedVersion) > 0 && | ||
| (followsPre || !isPrerelease(a.version))) | ||
| const offLine = intervening.filter((a) => minorLineOf(a.version) !== line) |
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.
Stacked on #42 (
fix/vendor-graph-semver-ordering) — merge #42 first. It stacks because this fix needs #42'sparseVersion, not because the two touch the same lines.The defect
track-minorwas implemented withmajorOf:A policy whose name states one contract and whose code honours another.
It is worse than "track-minor is really track-major". The two arms are exhaustive: if nothing newer shares the pinned major, then the newest release cannot either, so
majorMovedfires.releaseDistance > 0was therefore always stale — which istrack-latest. Three declared policies, two distinct behaviours, and the collapsed one is the default (defaultFreshnessPolicy ?? 'track-minor') that every pin in the live vendor register declares.Measured before the change — identical graph, both policies:
track-minortrack-latest0.4.46 → 0.4.47patch, inside minor0.4.46 → 0.5.0minor, leaves minor0.4.46 → 1.0.0major, leaves major0.4.46, nothing newer0 of 5 probe cases distinguished the two policies.
majorOfonly ever changed the rationale string, never a verdict.The semantics established
A pin on
x.y.*follows its own minor line. It accepts patch bumps within that line; it does not accept bumps that leave it.0.4.46 → 0.4.47in-lane0.4.470.4.46 → 0.5.0off-lane0.4.46 → 1.0.0off-lanecurrenthere never means "nothing happened upstream".releaseDistanceandlatestVersionstill report the real gap, and the rationale names every off-line release:Leaving the line stays a deliberate act — an owner editing the pin, or declaring
track-latest— not something the freshness plane proposes on the owner's behalf.The same defect one layer down
proposeRevendortookverdict.latestVersion. So a correctly staletrack-minorpin at0.4.46with both0.4.47and0.5.0upstream would still have proposed→ 0.5.0: the exact bump the policy refuses, sealed into anEffectRequest, under a rationale saying it was withheld. FixingstalenessOfalone would have moved the bug rather than closed it.StalenessVerdictnow carriespolicyTargetVersion— the newest release the policy will take (track-latest⇒ newest;pin-exact⇒ the pin;track-minor⇒ newest in-lane) — and the proposal uses it.track-latestis unchanged and still proposes the newest release; the split is per-policy, not global.Boundary rules, stated so they are not rediscovered as bugs
unknown) names nox.yline, so its state isunknown— the same answer, for the same reason, that an unobserved pin already gets. Derivingcurrentwould be exactly the guess the unobserved branch refuses to make. A register declaringcurrentover it becomes adispositionViolation, which is correct: you cannot follow a minor line from a sha.0.4.47-rc.1leaves0.4.46current), matching how semver ranges treat prereleases — opt-in, never inherited. A pin that is itself a prerelease does follow them.parseVersion, notsplit('.'), sov0.4.46,0.4.46+build.7and0.4all resolve to the0.4line, and1/1.0/1.0.0are one lane.Proof
The 9 new tests were run against both implementations:
Including
the live register is unaffected: 0.4.40 → 0.4.45 is all one 0.4 line— the fix must not quietly un-stale the real finding the plane was built to report. (It fails pre-fix only on the newpolicyTargetVersionassertion; the state was and remainsstale.)Full suite 426/426,
npm run typecheckclean,ts/distrebuilt and committed.Lane note
ts/src/vendor-graph.tsis also touched by #40 and #42.git merge-treeagainst both:The three PRs touch disjoint regions of the source: #42 the ingest + comparator, #40
analyzeVendorFreshness, this onestalenessOf+proposeRevendor. The only real overlap is the rebuiltts/dist, which git cannot auto-merge — whoever merges second runsnpm run build && git add ts/dist.