First public release

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-28 14:49:02 -04:00
co-authored by Claude Opus 5.5
commit 8fd6096649
14 changed files with 2104 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 2
[*.md]
trim_trailing_whitespace = false
+9
View File
@@ -0,0 +1,9 @@
node_modules/
dist/
*.log
.DS_Store
.env
.env.*
!.env.example
.npmrc
dist-cjs/
+6
View File
@@ -0,0 +1,6 @@
stages: [test]
test:
stage: test
image: node:22-alpine
script: [npm ci, npm run build, npm test]
+15
View File
@@ -0,0 +1,15 @@
# Changelog
## 0.1.0 - 2026-09-28
First public release under MIT.
- `rank()` orders capabilities by readiness gained per unit of effort,
`(100 - score) × weight ÷ effort`, not by score.
- Unassessed capabilities (`score: null`) are listed separately and never
ranked.
- `effort: 'unknown'` weighs the same as `medium`.
- Every entry carries its binding constraint and next action; an entry with
neither is reported as "a position, not advice".
- `rankingTable()` prints in rank order; `toReport()` and `formatRanking()`
produce an evidence-record report.
+21
View File
@@ -0,0 +1,21 @@
# Contributing
Issues and merge requests are welcome at
https://gitlab.com/extant2000/portfolio-advisor.
## Ground rules
- A test that cannot fail proves nothing. If you fix a bug, add a test and
check that it fails against the unfixed code before you submit.
- Measure, don't assume. Two modules with the same line count can still be
different programs.
- Keep dependencies minimal. Every new runtime dependency needs a reason.
- Explain why in comments, not what. The what is already in the code.
- A breaking change needs a major version bump and a note in CHANGELOG.md.
## Before you open a merge request
Run both of these and make sure they pass:
npm run build
npm test
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Extant 2000 LLC
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+97
View File
@@ -0,0 +1,97 @@
# PortfolioAdvisor
Rank a portfolio of capabilities by transition readiness, and say what would
move each one.
PortfolioAdvisor takes a list of scored capabilities and returns them ordered
by the best next move. It is designed to consume
`@extant2000/sustain-score` output, but it takes a plain shape, so any source
of scores can feed it. It is pure, with no clock and no I/O.
## What it is built against
A bad ranking is not the main risk. The risk is a ranking that reads as
authoritative when the numbers behind it are not comparable. There are three
ways that happens.
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. A single
ordered list cannot hold both, so unassessed capabilities are listed
separately and never interleaved. A `null` score is not zero.
2. A composite hides its binding constraint. "Reports: 6" tells you where
something sits, not what to do. Every entry carries its binding
constraint and next action. An entry with neither is reported as "a
position, not advice".
3. Effort is invisible in a score. Two capabilities at 45, one needing a key
rotation and one needing a rewrite, are not equally worth doing next.
So the ranking is by readiness gained per unit of effort, not by score.
Each entry gets a `leverage` value:
```
(100 - score) × weight ÷ effort
```
That is the headroom left, scaled by how much the capability matters, divided
by what the work costs. Effort weights are `trivial` 1, `small` 2, `medium` 4
and `large` 8. `unknown` counts as `medium`, not `trivial`. An unestimated
task is not a cheap one, and defaulting optimistically is how an unscoped item
reaches the top of a plan. Only a capability scoring 90 or more passes; the
rest are reported as work outstanding.
## Two orders
`formatRanking()` prints the ranked table first and then the findings
worst-first. The two answer different questions ("what should I do next" and
"what is most wrong"), and they are not the same order. A ranking whose
display order contradicts its own numbering is worse than no numbering, so the
table always prints #1 first.
## Usage
```ts
import { rank, rankingTable, formatRanking, type CapabilityStanding } from '@extant2000/portfolio-advisor'
const portfolio: CapabilityStanding[] = [
{ name: 'docs', score: 90, effort: 'small', bindingConstraint: 'none material' },
{ name: 'gateway', score: 85, effort: 'small', bindingConstraint: 'retry fix unmerged', nextAction: 'merge the retry fix' },
{ name: 'reports', score: 60, effort: 'large', bindingConstraint: 'scheduled exports promised, nothing runs them', nextAction: 'build the scheduler' },
{ name: 'billing', score: 80, effort: 'trivial', bindingConstraint: 'debug logging left on', nextAction: 'turn off debug logging' },
]
const { ranked, unassessed } = rank(portfolio)
console.log(rankingTable(portfolio))
// Table plus the evidence-record findings
console.log(formatRanking(portfolio))
```
`toReport(portfolio)` returns a `CapabilityReport` from
`@extant2000/evidence-record`. Each entry can also carry a `weight` (default
1) and a `where` that is cited as the source of the score.
## Sample output
Produced by running `rankingTable()` on the example above:
```
#1 billing 80/100 leverage 20 turn off debug logging
#2 gateway 85/100 leverage 7.5 merge the retry fix
#3 reports 60/100 leverage 5 build the scheduler
#4 docs 90/100 leverage 5 none material
```
The weakest capability is third. Ordering by score alone would have put the
large rebuild ahead of a change that is close to free. A capability with
`score: null` would appear below the table, unnumbered, as unassessed.
## Install
```
npm install @extant2000/portfolio-advisor
```
## License
MIT. Copyright (c) 2026 Extant 2000 LLC. See [LICENSE](LICENSE).
+11
View File
@@ -0,0 +1,11 @@
# Security policy
## Reporting a vulnerability
Please report vulnerabilities privately by email to security@extant2000.com.
Do not open a public issue or merge request for a security problem.
Include the affected version, a description of the issue, and steps or a test
that reproduce it if you have them.
We aim to acknowledge every report within 5 business days.
+1504
View File
File diff suppressed because it is too large Load Diff
+46
View File
@@ -0,0 +1,46 @@
{
"name": "@extant2000/portfolio-advisor",
"version": "0.1.0",
"private": false,
"description": "Ranks in-flight capabilities by transition readiness.",
"license": "MIT",
"type": "module",
"main": "./dist-cjs/index.js",
"files": [
"dist",
"dist-cjs",
"src",
"README.md",
"LICENSE"
],
"repository": {
"type": "git",
"url": "https://gitlab.com/extant2000/portfolio-advisor.git"
},
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist-cjs/index.js"
}
},
"scripts": {
"build": "tsc && tsc -p tsconfig.cjs.json && node -e \"require('fs').writeFileSync('dist-cjs/package.json', JSON.stringify({type:'commonjs'}))\"",
"test": "vitest run",
"prepublishOnly": "npm run build"
},
"devDependencies": {
"typescript": "^5.6.0",
"vitest": "^2.1.0"
},
"dependencies": {
"@extant2000/evidence-record": "^0.1.2"
},
"author": "Extant 2000 LLC",
"homepage": "https://gitlab.com/extant2000/portfolio-advisor",
"bugs": {
"url": "https://gitlab.com/extant2000/portfolio-advisor/-/issues"
}
}
+214
View File
@@ -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 }
+131
View File
@@ -0,0 +1,131 @@
import { describe, expect, it } from 'vitest'
import { overall, validateReport } from '@extant2000/evidence-record'
import { formatRanking, rank, toReport, type CapabilityStanding } from '../src/index.js'
// An example portfolio of four services, scored 0-100.
const suite: CapabilityStanding[] = [
{ name: 'docs', score: 90, effort: 'small', bindingConstraint: 'none material', where: 'quarterly scorecard' },
{ name: 'gateway', score: 85, effort: 'small', bindingConstraint: 'retry fix written but unmerged', nextAction: 'merge the retry fix', where: 'quarterly scorecard' },
{ name: 'reports', score: 60, effort: 'large', bindingConstraint: 'scheduled exports promised, nothing runs them', nextAction: 'build the scheduler', where: 'quarterly scorecard' },
{ name: 'billing', score: 80, effort: 'trivial', bindingConstraint: 'debug logging left on', nextAction: 'turn off debug logging', where: 'quarterly scorecard' },
]
describe('unassessed is not the bottom of the list', () => {
it('ranks unassessed capabilities separately, never interleaved', () => {
// "Nobody looked at this" and "we looked and it is weak" are opposite
// situations. One ordered list cannot hold both.
const r = rank([...suite, { name: 'newservice', score: null }])
expect(r.ranked.map(c => c.name)).not.toContain('newservice')
expect(r.unassessed.map(c => c.name)).toEqual(['newservice'])
})
it('reports an unassessed capability as not-assessed and says why it is unranked', () => {
const r = toReport([...suite, { name: 'newservice', score: null }])
const f = r.findings.find(x => x.id === 'newservice')!
expect(f.determination).toBe('not-assessed')
expect(f.detail).toContain('backwards, as advice')
// The roll-up is `fail`, not `not-assessed`: a real shortfall takes
// precedence over an unknown, which is the standard's defined order.
expect(overall(r)).toBe('fail')
})
it('rolls up to not-assessed when the ONLY entries are unassessed', () => {
expect(overall(toReport([{ name: 'newservice', score: null }]))).toBe('not-assessed')
})
it('does not let a null score sort as zero', () => {
const r = rank([{ name: 'unknown', score: null }, { name: 'weak', score: 10, effort: 'small' }])
expect(r.ranked[0]!.name).toBe('weak')
expect(r.ranked).toHaveLength(1)
})
})
describe('leverage, not score', () => {
it('prefers a cheap fix on a mid capability over an expensive one on a weak capability', () => {
// billing: (100-80)/1 = 20. reports: (100-60)/8 = 5.
// Ordering by score alone would recommend the rewrite.
const r = rank(suite)
expect(r.ranked[0]!.name).toBe('billing')
expect(r.ranked[0]!.leverage).toBeGreaterThan(r.ranked.find(c => c.name === 'reports')!.leverage!)
})
it('scales by strategic weight', () => {
const r = rank([
{ name: 'minor', score: 50, effort: 'small', weight: 1 },
{ name: 'major', score: 50, effort: 'small', weight: 3 },
])
expect(r.ranked[0]!.name).toBe('major')
})
it('treats unknown effort as medium, not as free', () => {
// An unestimated task is not a cheap one. Defaulting optimistically is
// how an unscoped item reaches the top of a plan.
const r = rank([
{ name: 'unscoped', score: 50 },
{ name: 'trivial-known', score: 50, effort: 'trivial' },
])
expect(r.ranked[0]!.name).toBe('trivial-known')
})
it('gives a near-finished capability little leverage however cheap the work', () => {
const r = rank([
{ name: 'nearly-done', score: 98, effort: 'trivial' },
{ name: 'midway', score: 60, effort: 'medium' },
])
expect(r.ranked[0]!.name).toBe('midway')
})
})
describe('a rank without a constraint is not advice', () => {
it('names the binding constraint and next action on each entry', () => {
const text = formatRanking(suite)
expect(text).toContain('debug logging left on')
expect(text).toContain('turn off debug logging')
expect(text).toContain('best next move: billing')
})
it('says so when no constraint was recorded', () => {
const f = toReport([{ name: 'x', score: 50 }]).findings[0]!
expect(f.detail).toContain('a position, not advice')
})
it('only a genuinely finished capability passes', () => {
expect(toReport([{ name: 'done', score: 95, effort: 'trivial' }]).findings[0]!.determination).toBe('pass')
expect(toReport([{ name: 'nearly', score: 89, effort: 'trivial' }]).findings[0]!.determination).toBe('fail')
})
})
describe('conformance with the evidence-record standard', () => {
it('an empty portfolio is not-assessed, never clean', () => {
expect(overall(toReport([]))).toBe('not-assessed')
})
it('every conclusion carries a citation', () => {
expect(validateReport(toReport(suite))).toEqual([])
})
it('carries the score into the citation as a measurement', () => {
const text = formatRanking(suite, { evidence: 'inline' })
expect(text).toContain('billing readiness = 80')
})
})
describe('the ranking prints in rank order', () => {
it('shows #1 first, even when a lower-ranked entry is more severe', () => {
// The shared renderer sorts worst-first, which is right for findings and
// wrong for a ranking. A ranking whose display order contradicts its own
// numbering is worse than no numbering.
const text = formatRanking(suite)
const table = text.split('\n\n')[0]!
expect(table.indexOf('#1')).toBeLessThan(table.indexOf('#2'))
expect(table.indexOf('#2')).toBeLessThan(table.indexOf('#3'))
expect(table.split('\n')[0]).toContain('billing')
})
it('lists unassessed capabilities under the ranked ones, without a number', () => {
const table = formatRanking([...suite, { name: 'newservice', score: null }]).split('\n\n')[0]!
expect(table).toContain('newservice')
expect(table).toContain('no score to rank from')
expect(table.indexOf('#1')).toBeLessThan(table.indexOf('newservice'))
})
})
+9
View File
@@ -0,0 +1,9 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "CommonJS",
"moduleResolution": "Node",
"outDir": "dist-cjs",
"declaration": false
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"compilerOptions": {
"target": "ES2022", "module": "ES2022", "moduleResolution": "bundler",
"declaration": true, "outDir": "dist", "rootDir": "src",
"strict": true, "skipLibCheck": true
},
"include": ["src/**/*.ts"]
}