First public release
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+214
@@ -0,0 +1,214 @@
|
||||
/**
|
||||
* PortfolioAdvisor — rank capabilities by transition readiness, and say what
|
||||
* would move each one.
|
||||
*
|
||||
* The failure this is built against is not a bad ranking. It is a ranking
|
||||
* that reads as authoritative when the numbers behind it are not comparable:
|
||||
*
|
||||
* 1. **Unassessed capabilities sort as bad.** A capability nobody scored
|
||||
* lands at the bottom next to one that was scored and found weak. Those
|
||||
* are opposite situations — one needs work, the other needs an
|
||||
* assessment — and a single ordered list cannot hold both. Unassessed
|
||||
* items are therefore ranked SEPARATELY and never interleaved.
|
||||
*
|
||||
* 2. **A composite hides its own binding constraint.** "Reports: 6" tells you
|
||||
* where it sits, not what to do. A ranking that cannot name the one
|
||||
* thing holding each capability down is a leaderboard, not advice.
|
||||
*
|
||||
* 3. **Effort is invisible in a score.** The capability at 45 that needs a
|
||||
* key rotation and the one at 45 that needs a rewrite are not equally
|
||||
* worth doing next. Ordering by score alone systematically recommends
|
||||
* the expensive one.
|
||||
*
|
||||
* Designed to consume `@extant2000/sustain-score` output, but takes a plain
|
||||
* shape so anything can feed it.
|
||||
*
|
||||
* Pure: no clock, no I/O.
|
||||
*/
|
||||
|
||||
import {
|
||||
type CapabilityReport,
|
||||
type Finding,
|
||||
type FormatOptions,
|
||||
evidence,
|
||||
formatReport as renderReport,
|
||||
} from '@extant2000/evidence-record'
|
||||
|
||||
/** How much work the binding constraint represents. */
|
||||
export type Effort = 'trivial' | 'small' | 'medium' | 'large' | 'unknown'
|
||||
|
||||
const EFFORT_WEIGHT: Record<Effort, number> = {
|
||||
trivial: 1,
|
||||
small: 2,
|
||||
medium: 4,
|
||||
large: 8,
|
||||
// Unknown effort is weighted as medium rather than as trivial: an
|
||||
// unestimated task is not a cheap one, and defaulting optimistically is
|
||||
// how an unscoped item reaches the top of a plan.
|
||||
unknown: 4,
|
||||
}
|
||||
|
||||
export interface CapabilityStanding {
|
||||
name: string
|
||||
/**
|
||||
* Readiness 0–100, or `null` when the capability was never assessed.
|
||||
* Null is NOT zero — see `rank()`.
|
||||
*/
|
||||
score: number | null
|
||||
/** The single thing holding it down. */
|
||||
bindingConstraint?: string
|
||||
/** The highest-leverage next action. */
|
||||
nextAction?: string
|
||||
effort?: Effort
|
||||
/** Strategic weight: how much this capability matters to the portfolio. */
|
||||
weight?: number
|
||||
/** Where the score came from — a scorecard, a report, a run. */
|
||||
where?: string
|
||||
}
|
||||
|
||||
export interface Ranked extends Omit<CapabilityStanding, 'score'> {
|
||||
/** Never null: an unassessed capability is not ranked at all. */
|
||||
score: number
|
||||
/** Readiness gain per unit of effort. Higher is a better next move. */
|
||||
leverage: number
|
||||
rank: number
|
||||
}
|
||||
|
||||
export interface Ranking {
|
||||
/** Assessed capabilities, best next move first. */
|
||||
ranked: Ranked[]
|
||||
/** Never assessed. Listed separately, never interleaved. */
|
||||
unassessed: CapabilityStanding[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Rank by leverage: how much readiness a unit of effort buys.
|
||||
*
|
||||
* `(100 - score) * weight / effort` — the headroom that remains, scaled by
|
||||
* how much the capability matters, divided by what it costs. A capability
|
||||
* already at 90 has little headroom left however cheap the work is, and a
|
||||
* large task on a low-weight capability sinks even with lots of headroom.
|
||||
*
|
||||
* Unassessed capabilities are NOT ranked. They have no score to compute
|
||||
* headroom from, and slotting them in at zero would put "we never looked at
|
||||
* this" above "we looked and it is genuinely weak" — which is exactly
|
||||
* backwards as advice.
|
||||
*/
|
||||
export function rank(standings: CapabilityStanding[]): Ranking {
|
||||
const list = standings ?? []
|
||||
const unassessed = list.filter(c => c.score === null)
|
||||
const assessed = list.filter((c): c is CapabilityStanding & { score: number } => c.score !== null)
|
||||
|
||||
const ranked = assessed
|
||||
.map(c => {
|
||||
const effort = EFFORT_WEIGHT[c.effort ?? 'unknown']
|
||||
const weight = c.weight ?? 1
|
||||
return { ...c, leverage: Math.round(((100 - c.score) * weight / effort) * 10) / 10 }
|
||||
})
|
||||
.sort((a, b) => b.leverage - a.leverage || a.score - b.score || a.name.localeCompare(b.name))
|
||||
.map((c, i) => ({ ...c, rank: i + 1 }))
|
||||
|
||||
return { ranked, unassessed }
|
||||
}
|
||||
|
||||
function standingEvidence(c: CapabilityStanding) {
|
||||
return c.where
|
||||
? evidence.measurement(`${c.name} readiness`, c.score, c.score === null ? 0 : 1, { note: c.where })
|
||||
: evidence.measurement(`${c.name} readiness`, c.score, c.score === null ? 0 : 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a report from a ranking.
|
||||
*
|
||||
* Every entry carries its binding constraint, because a ranking that cannot
|
||||
* say what is holding each capability down is a leaderboard rather than
|
||||
* advice. An entry with no constraint recorded says so rather than appearing
|
||||
* complete.
|
||||
*/
|
||||
export function toReport(standings: CapabilityStanding[]): CapabilityReport {
|
||||
const list = standings ?? []
|
||||
if (list.length === 0) {
|
||||
return {
|
||||
capability: 'PortfolioAdvisor',
|
||||
scope: 'no capabilities supplied',
|
||||
examined: 0,
|
||||
findings: [],
|
||||
notes: ['Nothing was supplied to rank.'],
|
||||
}
|
||||
}
|
||||
|
||||
const { ranked, unassessed } = rank(list)
|
||||
|
||||
const rankedFindings: Finding[] = ranked.map(c => ({
|
||||
id: c.name,
|
||||
summary: `#${c.rank} ${c.name} — ${c.score}/100, leverage ${c.leverage}${c.effort ? ` (${c.effort} effort)` : ''}`,
|
||||
// A ranking entry is advice, not a verdict on the capability. Only a
|
||||
// genuinely finished capability passes; everything else is work
|
||||
// outstanding, which is a `fail` against the readiness criterion.
|
||||
determination: c.score >= 90 ? 'pass' : 'fail',
|
||||
severity: c.score < 40 ? 'critical' : c.score < 70 ? 'high' : 'info',
|
||||
detail: c.bindingConstraint
|
||||
? `Binding constraint: ${c.bindingConstraint}${c.nextAction ? ` → ${c.nextAction}` : ''}`
|
||||
: 'No binding constraint recorded. A rank without one is a position, not advice — it does not say what to do.',
|
||||
evidence: [standingEvidence(c)],
|
||||
}))
|
||||
|
||||
const unassessedFindings: Finding[] = unassessed.map(c => ({
|
||||
id: c.name,
|
||||
summary: `${c.name} has never been assessed`,
|
||||
determination: 'not-assessed',
|
||||
severity: 'high',
|
||||
detail: 'Not ranked. An unassessed capability has no headroom to compute, and placing it at zero would rank "nobody looked" above "we looked and it is weak" — backwards, as advice.',
|
||||
evidence: [],
|
||||
}))
|
||||
|
||||
return {
|
||||
capability: 'PortfolioAdvisor',
|
||||
scope: `${list.length} capabilit${list.length === 1 ? 'y' : 'ies'}`,
|
||||
examined: list.length,
|
||||
findings: [...unassessedFindings, ...rankedFindings],
|
||||
notes: [
|
||||
`${ranked.length} ranked by leverage (headroom x weight / effort), ${unassessed.length} unassessed and excluded from the ranking`,
|
||||
ranked.length > 0
|
||||
? `best next move: ${ranked[0]!.name} — ${ranked[0]!.nextAction ?? 'no action recorded'}`
|
||||
: 'no capability could be ranked',
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The ranking itself, in rank order.
|
||||
*
|
||||
* Printed separately from the findings because the shared renderer sorts
|
||||
* worst-first, which is right for findings and wrong for a ranked list — it
|
||||
* would show #2 above #1 whenever #2 scored lower. A ranking whose display
|
||||
* order contradicts its own numbering is worse than no numbering.
|
||||
*/
|
||||
export function rankingTable(standings: CapabilityStanding[]): string {
|
||||
const { ranked, unassessed } = rank(standings ?? [])
|
||||
const out: string[] = []
|
||||
|
||||
for (const c of ranked) {
|
||||
const action = c.nextAction ?? c.bindingConstraint ?? 'no action recorded'
|
||||
out.push(` #${String(c.rank).padEnd(2)} ${c.name.padEnd(16)} ${String(c.score).padStart(3)}/100 leverage ${String(c.leverage).padStart(5)} ${action}`)
|
||||
}
|
||||
for (const c of unassessed) {
|
||||
out.push(` -- ${c.name.padEnd(16)} —/100 ${'unassessed'.padStart(14)} no score to rank from`)
|
||||
}
|
||||
return out.join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a portfolio ranking for a terminal or a briefing.
|
||||
*
|
||||
* The rank-ordered table first, then the findings worst-first. They answer
|
||||
* different questions — "what should I do next" and "what is most wrong" —
|
||||
* and they are not the same order.
|
||||
*/
|
||||
export function formatRanking(standings: CapabilityStanding[], opts: FormatOptions = {}): string {
|
||||
const table = rankingTable(standings)
|
||||
const report = renderReport(toReport(standings), opts)
|
||||
return table ? `${table}\n\n${report}` : report
|
||||
}
|
||||
|
||||
export { EFFORT_WEIGHT }
|
||||
Reference in New Issue
Block a user