Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified ts/dist/index.d.mts
Binary file not shown.
Binary file modified ts/dist/index.d.ts
Binary file not shown.
Binary file modified ts/dist/index.js
Binary file not shown.
Binary file modified ts/dist/index.mjs
Binary file not shown.
146 changes: 146 additions & 0 deletions ts/src/vendor-graph.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -848,3 +848,149 @@ test('a source with no repo produces no edge to an empty repository id', () => {
assert.equal(store.getNode(emptyRepo), undefined, 'no node was created for the empty repository id')
assert.deepEqual(store.in(emptyRepo, VFP_EDGE.producedBy), [], 'and nothing points at it')
})

// ─── 9 ─ `track-minor` means the MINOR 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 honoured another. 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 `majorMoved` fires — which made
* `releaseDistance > 0` always stale. `track-minor` was `track-latest`, wearing a major-shaped
* rationale string. Three declared policies, two distinct behaviours, and the collapsed one is
* the DEFAULT that every pin in the live register declares.
*
* These tests fix the contract the name states: a pin on `x.y.*` follows its own minor line.
*/
const minorLineFixture = (releases: string[], pinned: string, policy = 'track-minor'): HellGraphStore => {
const store = new HellGraphStore(new AtomSpace(`vendor-graph-line-${pinned}-${releases.join('_')}-${policy}`, false))
ingestVendorFreshness(store, {
manifest_id: 'vendor-freshness-minor-line',
sources: [{
source_id: 'engine',
repo: 'SocioProphet/hellgraph',
artifact_kind: 'npm-tarball',
version_scheme: 'semver',
releases: releases.map((version) => ({ version })),
}],
artifacts: [{
artifact_id: 'engine@probe',
source_id: 'engine',
consumer_repo: 'SocioProphet/prophet-platform',
consumer_app: 'apps/probe',
vendored_version: pinned,
freshness_policy: policy,
disposition: 'current',
}],
})
return store
}
const LINE_PIN = vfpId.pin('engine@probe')

test('track-minor ACCEPTS a patch bump inside the pinned minor: 0.4.46 → 0.4.47', () => {
const v = stalenessOf(minorLineFixture(['0.4.46', '0.4.47'], '0.4.46'), LINE_PIN)
assert.equal(v.freshnessState, 'stale', '0.4.47 is on the 0.4 line the pin follows — take it')
assert.equal(v.policyTargetVersion, '0.4.47')
assert.match(v.rationale, /on the pinned 0\.4 line, newest 0\.4\.47/)
})

test('track-minor REFUSES a minor bump inside the pinned major: 0.4.46 → 0.5.0', () => {
const v = stalenessOf(minorLineFixture(['0.4.46', '0.5.0'], '0.4.46'), LINE_PIN)
assert.equal(v.freshnessState, 'current', '0.5.0 left the 0.4 line — not this pin\'s to take')
assert.equal(v.releaseDistance, 1, 'and the distance is still REPORTED: current ≠ nothing happened')
assert.equal(v.policyTargetVersion, '0.4.46', 'so no cross-minor target is proposed')
assert.match(v.rationale, /0\.5\.0.*not followed under track-minor/)
})

test('track-minor is no longer track-latest — the two now disagree', () => {
// The differential the old implementation could not produce: identical graph, both policies.
const cases: [string[], string, string, string][] = [
[['0.4.46', '0.4.47'], '0.4.46', 'stale', 'stale'],
[['0.4.46', '0.5.0'], '0.4.46', 'current', 'stale'],
[['0.4.46', '1.0.0'], '0.4.46', 'current', 'stale'],
[['0.4.46', '0.4.47', '0.5.0'], '0.4.46', 'stale', 'stale'],
]
for (const [releases, pinned, wantMinor, wantLatest] of cases) {
assert.equal(stalenessOf(minorLineFixture(releases, pinned), LINE_PIN).freshnessState, wantMinor,
`track-minor over ${releases.join('→')}`)
assert.equal(stalenessOf(minorLineFixture(releases, pinned, 'track-latest'), LINE_PIN).freshnessState, wantLatest,
`track-latest over ${releases.join('→')}`)
}
})

test('a stale track-minor pin proposes the newest IN-LANE release, not the newest release', () => {
// 0.4.47 and 0.5.0 both exist. The pin IS stale — 0.4.47 is on its line — and the proposal it
// earns is `→ 0.4.47`. Proposing `→ 0.5.0` would hand a human the cross-minor bump the policy
// exists to withhold, under a rationale that says it was withheld.
const store = minorLineFixture(['0.4.46', '0.4.47', '0.5.0'], '0.4.46')
const v = stalenessOf(store, LINE_PIN)
assert.equal(v.freshnessState, 'stale')
assert.equal(v.latestVersion, '0.5.0', 'the newest release is still reported as such')
assert.equal(v.policyTargetVersion, '0.4.47', 'but the policy will only take the 0.4 line')

const p = proposeRevendor(store, LINE_PIN, { requestedAt: '2026-07-30T00:00:00Z' })
assert.equal(p.effectRequest.parameters['toVersion'], '0.4.47')
assert.equal(p.effectRequest.idempotencyKey, 'engine@probe@0.4.46->0.4.47')
assert.deepEqual(p.contractViolations, [])
})

test('track-latest still proposes the newest release — the split is per-policy, not global', () => {
const store = minorLineFixture(['0.4.46', '0.4.47', '0.5.0'], '0.4.46', 'track-latest')
const v = stalenessOf(store, LINE_PIN)
assert.equal(v.policyTargetVersion, '0.5.0')
assert.equal(proposeRevendor(store, LINE_PIN, { requestedAt: '2026-07-30T00:00:00Z' })
.effectRequest.parameters['toVersion'], '0.5.0')
})

test('the minor line is parsed, not string-split: v-prefix, build metadata, missing patch', () => {
// `split('.')[0]` on `v0.4.46` yields `v0`; on `0.4.46+build.7` the LINE is still 0.4. These all
// name a point on the 0.4 line, and `parseVersion` is what knows that.
for (const [pinned, newer] of [['v0.4.46', '0.4.47'], ['0.4.46+build.7', 'v0.4.47+build.8'], ['0.4', '0.4.1']]) {
const v = stalenessOf(minorLineFixture([pinned!, newer!], pinned!), LINE_PIN)
assert.equal(v.freshnessState, 'stale', `${pinned} → ${newer} is an in-lane bump`)
}
// …and 1 / 1.0 / 1.0.0 are one lane, so 1.1.0 leaves it.
assert.equal(stalenessOf(minorLineFixture(['1', '1.1.0'], '1'), LINE_PIN).freshnessState, 'current')
assert.equal(stalenessOf(minorLineFixture(['1', '1.0.1'], '1'), LINE_PIN).freshnessState, 'stale')
})

test('a prerelease does not make a stable track-minor pin stale — but does for a prerelease pin', () => {
assert.equal(stalenessOf(minorLineFixture(['0.4.46', '0.4.47-rc.1'], '0.4.46'), LINE_PIN).freshnessState,
'current', 'prereleases are opt-in, as in every semver range')
assert.equal(stalenessOf(minorLineFixture(['0.4.47-rc.1', '0.4.47-rc.2'], '0.4.47-rc.1'), LINE_PIN).freshnessState,
'stale', 'a pin that is itself a prerelease follows them')
assert.equal(stalenessOf(minorLineFixture(['0.4.47-rc.1', '0.4.47'], '0.4.47-rc.1'), LINE_PIN).freshnessState,
'stale', 'and the release the prerelease led to is in-lane')
})

test('a track-minor pin that is not release-shaped is UNKNOWN, not a guess', () => {
// A sha names no x.y line. Deriving `current` would be the same silent guess the unobserved
// branch already refuses to make.
const store = new HellGraphStore(new AtomSpace('vendor-graph-shapeless', false))
ingestVendorFreshness(store, {
manifest_id: 'vendor-freshness-shapeless',
sources: [{ source_id: 'engine', repo: 'SocioProphet/hellgraph', releases: [{ version: '7d74db8' }, { version: '0.4.47' }] }],
artifacts: [{
artifact_id: 'engine@probe', source_id: 'engine',
consumer_repo: 'SocioProphet/prophet-platform', consumer_app: 'apps/probe',
vendored_version: '7d74db8', freshness_policy: 'track-minor', disposition: 'current',
}],
})
const v = stalenessOf(store, LINE_PIN)
assert.equal(v.freshnessState, 'unknown')
assert.equal(v.dispositionAgrees, false, 'declaring `current` over an undecidable state is a violation')
assert.match(v.rationale, /not release-shaped/)
})

test('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 it was built to keep reporting.
const v = stalenessOf(fixture(), SERVICE_PIN)
assert.equal(v.freshnessState, 'stale')
assert.equal(v.policyTargetVersion, '0.4.45', 'every intervening release is on the 0.4 line')
assert.equal(v.latestVersion, '0.4.45')
})
109 changes: 96 additions & 13 deletions ts/src/vendor-graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -734,6 +734,16 @@ export interface StalenessVerdict {
/** The artifacts crossed, in release order. `releaseDistance === intervening.length`. */
intervening: InterveningArtifact[]
freshnessPolicy: FreshnessPolicy
/**
* The newest release the POLICY is willing to take — which is not always `latestVersion`.
*
* `track-latest` takes the newest release; `pin-exact` takes nothing, so it is the pinned
* version; `track-minor` takes the newest release **on the pinned `x.y` line**, which is
* `0.4.47` even when `0.5.0` exists. This is the version `proposeRevendor` proposes: without
* it a policy that correctly REFUSES a cross-minor bump would still emit a proposal to make
* it, and the refusal would be a verdict nobody acts on.
*/
policyTargetVersion: string
/** DERIVED, per the policy. */
freshnessState: FreshnessState
/** DECLARED in the register. */
Expand All @@ -750,21 +760,65 @@ export interface StalenessVerdict {
guardInvokedByCi?: boolean
}

const majorOf = (v: string): string => v.split('.')[0] ?? v
/**
* The `x.y` line a version belongs to — the lane a `track-minor` pin follows — or `undefined`
* if the version is not release-shaped and therefore has no lane.
*
* Derived through `parseVersion`, not `split('.')`: `v0.4.46`, `0.4.46+build.7` and `0.4` all
* name a point on the `0.4` line, and a commit sha or the literal `unknown` names no line at
* all. A missing minor is `0` (`1` and `1.0` are the same lane), matching `compareVersions`,
* which already treats a missing component as `0`.
*/
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 +772 to +776

/** Is `v` a prerelease (`1.0.0-rc.1`)? Non-release-shaped strings are not. */
const isPrerelease = (v: string): boolean => parseVersion(v)?.prerelease !== undefined

/**
* Derive the staleness of one pin from the graph.
*
* PURE — reads only. Returns the release distance and the intervening artifacts, because the
* actionable question is never "is it stale" but "how far, across what".
*
* Policy semantics are the register's, implemented verbatim:
* Policy semantics are the register's:
* • `pin-exact` — never stale, but ONLY if upstream has actually been OBSERVED. A pin to
* something nobody has looked at is `unknown`, not `current` — it is an
* unknown wearing a pin's clothes.
* • `track-minor` — stale if a newer artifact shares the pinned major, or if the newest
* artifact's major differs at all.
* • `track-minor` — the pin follows ONE MINOR LINE, `x.y.*`. Stale when a newer release lands
* ON that line; NOT stale when the only newer releases have left it.
* • `track-latest` — stale if the distance is greater than zero.
*
* ── `track-minor` used to mean `track-latest` ───────────────────────────────────────────
* The previous implementation was
*
* const sameMajorNewer = intervening.some((a) => majorOf(a.version) === majorOf(pinnedVersion))
* const majorMoved = releaseDistance > 0 && majorOf(latestVersion) !== majorOf(pinnedVersion)
* freshnessState = sameMajorNewer || majorMoved ? 'stale' : 'current'
*
* — MAJOR, not minor, and the two arms between them are exhaustive: if nothing newer shares the
* pinned major then the newest cannot either, so `majorMoved` fires. `releaseDistance > 0` was
* therefore ALWAYS stale, and `track-minor` was `track-latest` with a major-shaped rationale
* string attached. Three declared policies, two distinct behaviours. A register author writing
* `freshness_policy: track-minor` — the DEFAULT, and what every pin in the live vendor register
* declares — was silently opted in to cross-minor bumps: a pin at `0.4.46` was reported stale
* against `0.5.0` and a re-vendor proposal to `0.5.0` was emitted and sealed for it.
*
* Now: a `track-minor` pin at `0.4.46` is stale against `0.4.47` and CURRENT against `0.5.0`.
* Leaving the line is a deliberate act — an owner editing the pin, or declaring `track-latest` —
* not something the freshness plane proposes on the owner's behalf. The rationale names the
* off-line releases anyway, so `current` here never means "nothing happened upstream".
*
* Two boundary rules, both stated so they are not rediscovered as bugs:
* · A pin whose version is NOT release-shaped (a sha, `unknown`) has no minor line to follow,
* so its state is `unknown`, not a guess in either direction — the same answer this function
* already gives an unobserved pin, for the same reason.
* · A PRERELEASE does not make a stable pin stale (`0.4.47-rc.1` leaves `0.4.46` current),
* matching how semver ranges treat prereleases: opt-in, never inherited. A pin that is
* itself a prerelease does follow them.
*/
export function stalenessOf(store: HellGraphStore, pinId: string): StalenessVerdict {
const pin = store.getNode(pinId)
Expand All @@ -789,6 +843,8 @@ export function stalenessOf(store: HellGraphStore, pinId: string): StalenessVerd
const observed = releaseDistance > 0 || store.in(pinnedArtifact.id, VFP_EDGE.supersededBy).length > 0
let freshnessState: FreshnessState
let rationale: string
// Defaults to the pinned version: a policy that takes nothing proposes nothing.
let policyTargetVersion = pinnedVersion
if (!observed) {
freshnessState = 'unknown'
rationale = `no upstream release history for ${pinnedVersion} in the graph — unobserved, not current`
Expand All @@ -797,18 +853,40 @@ export function stalenessOf(store: HellGraphStore, pinId: string): StalenessVerd
rationale = `pin-exact against an observed upstream: ${releaseDistance} newer release(s) exist and are deliberately not taken`
} else if (freshnessPolicy === 'track-latest') {
freshnessState = releaseDistance > 0 ? 'stale' : 'current'
policyTargetVersion = latestVersion
rationale = releaseDistance > 0
? `track-latest: ${releaseDistance} release(s) behind ${latestVersion}`
: `track-latest: pinned at the newest release ${pinnedVersion}`
} else {
const sameMajorNewer = intervening.some((a) => majorOf(a.version) === majorOf(pinnedVersion))
const majorMoved = releaseDistance > 0 && majorOf(latestVersion) !== majorOf(pinnedVersion)
freshnessState = sameMajorNewer || majorMoved ? 'stale' : 'current'
rationale = freshnessState === 'stale'
? (sameMajorNewer
? `track-minor: ${releaseDistance} newer release(s) share major ${majorOf(pinnedVersion)}, newest ${latestVersion}`
: `track-minor: newest release ${latestVersion} changes major from ${majorOf(pinnedVersion)}`)
: `track-minor: nothing newer within major ${majorOf(pinnedVersion)}`
const line = minorLineOf(pinnedVersion)
if (line === undefined) {
// No lane can be derived, so neither `stale` nor `current` is a fact. Saying either would
// be inventing one; `unknown` is the state this function already has for exactly that.
freshnessState = 'unknown'
rationale = `track-minor: pinned version '${pinnedVersion}' is not release-shaped, so it names no ` +
`x.y line to follow — undecidable, not current (declare pin-exact or track-latest, or pin a release)`
} else {
// In-lane: same x.y line, strictly newer by PRECEDENCE (not by chain position — a register
// may declare its releases out of order), and not a prerelease unless the pin is one.
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)
Comment on lines +871 to +876
freshnessState = onLine.length > 0 ? 'stale' : 'current'
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 +878 to +881
const offLineNote = offLine.length > 0
? ` (${offLine.length} newer release(s) have left the ${line} line — ${offLine.map((a) => a.version).join(', ')} — ` +
`not followed under track-minor)`
: ''
rationale = freshnessState === 'stale'
? `track-minor: ${onLine.length} newer release(s) on the pinned ${line} line, newest ${newestOnLine}${offLineNote}`
: `track-minor: nothing newer on the pinned ${line} line${offLineNote || ` (newest overall ${latestVersion})`}`
}
}

// Agreement, not enforcement of freshness. A `current` declaration over a `stale` derivation is
Expand All @@ -829,6 +907,7 @@ export function stalenessOf(store: HellGraphStore, pinId: string): StalenessVerd
releaseDistance,
intervening,
freshnessPolicy,
policyTargetVersion,
freshnessState,
disposition,
dispositionAgrees,
Expand Down Expand Up @@ -1075,7 +1154,11 @@ export function proposeRevendor(store: HellGraphStore, pinId: string, opts: Prop
const risk = contractCrossingRiskOf(store, pinId)
const pin = store.getNode(pinId)!

const toVersion = str(pin.properties['targetVersion']) ?? verdict.latestVersion
// `policyTargetVersion`, not `latestVersion`. They differ exactly when the policy declines the
// newest release — a `track-minor` pin at 0.4.46 with 0.4.47 and 0.5.0 upstream is stale, and
// the proposal it earns is `→ 0.4.47`. Proposing `→ 0.5.0` there would hand a human the very
// cross-minor bump the policy exists to withhold, over a rationale that says it was withheld.
const toVersion = str(pin.properties['targetVersion']) ?? verdict.policyTargetVersion
const location = verdict.artifactPath !== undefined
? `${verdict.consumerRepo}/${verdict.artifactPath}`
: undefined
Expand Down