First public release

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-28 14:49:03 -04:00
co-authored by Claude Opus 5.5
commit a4632842f0
17 changed files with 2599 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]
+19
View File
@@ -0,0 +1,19 @@
# Changelog
## 0.2.0 - 2026-09-28
First public release under MIT.
- `scoreDimension()`, `scoreProduct()` and `scoreSuite()` apply a
five-dimension readiness rubric with hard caps and six bands.
- The lowest open cap on a dimension wins; an unknown cap id throws.
- A score above 70 with no evidence is held at 70 and reported as NOT
ASSESSED.
- The overall score is the weakest dimension; the average is reported
alongside and labelled as such.
- Suite-wide caps apply before the tier-weighted average.
- A cap can cite the evidence that establishes it; an uncited cap is still
applied and the report says so.
- `productReport()`, `suiteReport()`, `formatProduct()` and `formatSuite()`
produce an evidence-record report.
- Worked example in `examples/cap-by-worst-defect.ts`.
+21
View File
@@ -0,0 +1,21 @@
# Contributing
Issues and merge requests are welcome at
https://gitlab.com/extant2000/sustain-score.
## 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.
+104
View File
@@ -0,0 +1,104 @@
# SustainScore
A production-readiness score that separates demo-ware from software that is
ready to hand over, with the rubric's caps enforced in code.
SustainScore encodes a five-dimension readiness rubric (security, bugs,
compliance, consistency, usability) with six bands from Prototype to Mature
and a table of hard caps. You supply the scorer's raw judgement per dimension,
the defects that are currently open, and the evidence. It returns scores that
apply the rubric's anti-inflation rules mechanically.
## Why
A rubric kept as prose gets applied charitably. A scorer can write down, in
the same row, that an open defect caps Security at 45 and still record 70.
Nothing in a document stops that. Encoding the rubric does.
The worked example in `examples/cap-by-worst-defect.ts` scores a fictional
product whose security review notes an exposed API key that was never
rotated. Run it with `npx tsx examples/cap-by-worst-defect.ts`:
```
Hand-scored vs rubric-enforced
security raw 70 -> 45 Beta <- CAPPED by sec.leaked-secret (45)
bugs raw 68 -> 68 Launch-capable, with debt
compliance raw 73 -> 73 Launch-capable, with debt
consistency raw 68 -> 68 Launch-capable, with debt
usability raw 68 -> 68 Launch-capable, with debt
hand-scored overall : ~69
enforced overall : 45 (Beta)
weakest : security
average : 64.4 (reported, never the headline)
delta : -24
```
A 24-point overstatement and a band change, from one cap a human reader
skipped.
## What it enforces
| Rule | Enforcement |
|---|---|
| Cap by the worst defect | The lowest open cap on a dimension wins. One serious hole is not averaged away by ten strengths. |
| Evidence required above 70 | A score above 70 with no cited evidence is held at 70 and reported as NOT ASSESSED, not as a pass. Unverified is treated as absent. |
| Overall is the lowest dimension | `overall` is the minimum. The average is reported alongside it and labelled so it cannot be read as the headline. |
| Suite caps apply before averaging | A shared platform flaw hits every product. Averaging first would let healthy products dilute it. |
An unknown cap id throws. Dropping a mistyped cap without a word would
inflate the score, which is the exact failure this library prevents. A cap
assigned to the wrong dimension also throws, and so does a product with a
missing dimension.
Caps can cite what establishes them: `openCaps` takes a bare id or
`{ id, evidence }`. A cap that cites nothing is still applied, and the report
says it cited nothing. An unproven cap is a smaller error than an unapplied
one.
The full cap table is exported as `CAP_RULES`, the bands as `BANDS`.
## Usage
```ts
import { scoreProduct, scoreSuite, formatProduct } from '@extant2000/sustain-score'
const result = scoreProduct({
product: 'example-app',
tier: 'A',
dimensions: {
security: { raw: 70, openCaps: ['sec.leaked-secret'], evidence: [{ ref: 'docs/security-review.md:14' }] },
bugs: { raw: 68, evidence: [{ ref: 'test/api.test.ts' }] },
compliance: { raw: 73, evidence: [{ ref: 'docs/compliance-checklist.md' }] },
consistency: { raw: 68, evidence: [{ ref: 'shared/ui/' }] },
usability: { raw: 68, evidence: [{ ref: 'reports/accessibility-scan.json' }] },
},
})
result.overall // 45, the lowest dimension
result.weakest // 'security'
result.band.name // 'Beta'
console.log(formatProduct(result))
```
`scoreSuite(products, { suiteCaps })` rolls several products up with tier
weighting (A = 2, B = 1.5, C = 1). `productReport()` and `suiteReport()`
return a `CapabilityReport` from `@extant2000/evidence-record`, and
`formatProduct()` and `formatSuite()` render one. Evidence can be the
evidence-record shape or a plain `{ ref, note }`, where a `path:line` ref
becomes a file citation.
To rank several scored products by what to fix next, see
`@extant2000/portfolio-advisor`.
## Install
```
npm install @extant2000/sustain-score
```
## 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.
+66
View File
@@ -0,0 +1,66 @@
/**
* Worked example: cap by the worst defect.
*
* A hand-scored assessment of a fictional product. The scorer rated Security
* at 70 and noted, in the same row, an open defect that the rubric caps at 45
* (a secret that was exposed and never rotated). A human reading prose can be
* charitable and skip the cap. The library cannot: the lowest open cap wins,
* and the product's overall score is its weakest dimension, not the average.
*
* Run: npx tsx examples/cap-by-worst-defect.ts
*/
import { scoreProduct, type DimensionAssessment, type DimensionKey } from '../src/index.js'
/** The overall score a hand-scored summary might report: roughly the average. */
const HAND_SCORED_OVERALL = 69
const dimensions: Record<DimensionKey, DimensionAssessment> = {
security: {
raw: 70,
// The scorer named this cap and still wrote 70.
openCaps: [{
id: 'sec.leaked-secret',
evidence: [{ ref: 'docs/security-review.md:14', note: 'exposed API key not yet rotated' }],
}],
evidence: [{ ref: 'docs/security-review.md' }],
bindingConstraint: 'exposed API key not yet rotated',
nextAction: 'rotate the key and redeploy',
},
bugs: {
raw: 68,
evidence: [{ ref: 'test/api.test.ts' }],
bindingConstraint: 'API handlers lack behaviour tests',
},
compliance: {
raw: 73,
evidence: [{ ref: 'docs/compliance-checklist.md' }],
},
consistency: {
raw: 68,
evidence: [{ ref: 'shared/ui/' }],
},
usability: {
raw: 68,
evidence: [{ ref: 'reports/accessibility-scan.json' }],
bindingConstraint: 'accessibility findings not yet triaged',
},
}
const result = scoreProduct({ product: 'example-app', tier: 'A', dimensions })
console.log('Hand-scored vs rubric-enforced\n')
for (const d of result.dimensions) {
const flag = d.boundBy ? ` <- CAPPED by ${d.boundBy.id} (${d.boundBy.max})` : ''
console.log(
` ${d.dimension.padEnd(12)} raw ${String(d.raw).padStart(3)} -> ${String(d.score).padStart(3)} ${d.band.name}${flag}`,
)
}
console.log(`\n hand-scored overall : ~${HAND_SCORED_OVERALL}`)
console.log(` enforced overall : ${result.overall} (${result.band.name})`)
console.log(` weakest : ${result.weakest}`)
console.log(` average : ${result.average} (reported, never the headline)`)
console.log(` delta : ${result.overall - HAND_SCORED_OVERALL}`)
if (result.overall < HAND_SCORED_OVERALL) {
console.log('\n The hand-scored number is higher than the rubric permits.')
}
+1504
View File
File diff suppressed because it is too large Load Diff
+44
View File
@@ -0,0 +1,44 @@
{
"name": "@extant2000/sustain-score",
"version": "0.2.0",
"private": false,
"description": "Maturity score separating demo-ware from transition-ready.",
"license": "MIT",
"type": "module",
"main": "dist/index.js",
"files": [
"dist",
"src",
"examples",
"README.md",
"LICENSE"
],
"repository": {
"type": "git",
"url": "https://gitlab.com/extant2000/sustain-score.git"
},
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"scripts": {
"build": "tsc",
"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/sustain-score",
"bugs": {
"url": "https://gitlab.com/extant2000/sustain-score/-/issues"
}
}
+3
View File
@@ -0,0 +1,3 @@
export * from './rubric.js'
export * from './score.js'
export * from './report.js'
+97
View File
@@ -0,0 +1,97 @@
import {
type CapabilityReport,
type Finding,
type FormatOptions,
evidence as ev,
formatReport as renderReport,
} from '@extant2000/evidence-record'
import type { DimensionScore, ProductScore, SuiteScore } from './score.js'
import { EVIDENCE_THRESHOLD } from './rubric.js'
/**
* One finding per dimension, carrying what set the number.
*
* The previous output said `CAPPED sec.leaked-secret` and stopped there. A
* reader could not tell whether the cap was still warranted, which is how a
* cap ends up recorded but not applied. A cap now cites what establishes it,
* and a cap that cites nothing says so.
*/
function findingFor(product: string, d: DimensionScore): Finding {
const id = `${product}.${d.dimension}`
if (d.boundBy) {
return {
id,
summary: `${d.dimension} capped at ${d.score} (assessed ${d.raw}) — ${d.boundBy.description}`,
determination: 'fail',
severity: d.score < 50 ? 'critical' : 'high',
detail: d.nextAction ?? d.bindingConstraint,
evidence: d.boundBy.evidence.length > 0
? d.boundBy.evidence
// Loud rather than silent: an uncited cap is still applied, because
// the cap being wrong is a better failure than the cap being skipped.
: [ev.document(`Cap rule ${d.boundBy.id}`, undefined, undefined,
'No evidence was recorded for this cap. It is applied anyway — an unproven cap is a smaller error than an unapplied one.')],
}
}
if (d.evidenceWithheld) {
return {
id,
summary: `${d.dimension} held at ${EVIDENCE_THRESHOLD} (assessed ${d.raw}) — no evidence was cited`,
determination: 'not-assessed',
severity: 'high',
detail: `A score above ${EVIDENCE_THRESHOLD} requires evidence. Unverified is treated as absent, so the assessed value was not granted.`,
evidence: [],
}
}
return {
id,
summary: `${d.dimension} scored ${d.score} (${d.band.name})`,
determination: 'pass',
severity: 'info',
detail: d.bindingConstraint,
evidence: d.evidence.length > 0
? d.evidence
: [ev.document(`${d.dimension} assessment`, undefined, undefined,
`At or below ${EVIDENCE_THRESHOLD}, evidence is not required by the rubric.`)],
}
}
/** Convert one product's score into the portfolio-standard report shape. */
export function productReport(p: ProductScore): CapabilityReport {
return {
capability: 'SustainScore',
scope: `${p.product} (${p.tier}) — ${p.dimensions.length} dimensions`,
examined: p.dimensions.length,
findings: p.dimensions.map(d => findingFor(p.product, d)),
notes: [
`overall ${p.overall} (${p.band.name}), set by the weakest dimension: ${p.weakest}`,
// Printed second and labelled, so it cannot be mistaken for the headline.
`average ${p.average} — reported because the rubric asks for it, never as the headline`,
],
}
}
/** Convert a suite roll-up into the portfolio-standard report shape. */
export function suiteReport(s: SuiteScore): CapabilityReport {
return {
capability: 'SustainScore',
scope: `${s.products.length} products, weighted by tier`,
examined: s.products.reduce((n, p) => n + p.dimensions.length, 0),
findings: s.products.flatMap(p => p.dimensions.map(d => findingFor(p.product, d))),
notes: [
`suite overall ${s.overall} (${s.band.name}), set by the weakest dimension: ${s.weakest}`,
`average ${s.average} — suite-wide caps are applied BEFORE averaging, so healthy products cannot dilute a shared flaw`,
],
}
}
export function formatProduct(p: ProductScore, opts: FormatOptions = {}): string {
return renderReport(productReport(p), opts)
}
export function formatSuite(s: SuiteScore, opts: FormatOptions = {}): string {
return renderReport(suiteReport(s), opts)
}
+118
View File
@@ -0,0 +1,118 @@
/**
* Production-Readiness Rubric v1.0 — encoded.
*
* A rubric kept as prose and applied by hand gets applied charitably. A scorer
* can write down a defect that caps a dimension at 45 and still record 70 in
* the same row. Encoding the rubric is not a formatting exercise: a charitable
* scorer is the failure mode this module removes.
*/
export type BandName =
| 'Prototype' | 'Alpha' | 'Beta'
| 'Launch-capable, with debt' | 'Production' | 'Mature'
export interface Band {
name: BandName
min: number
max: number
meaning: string
}
/** Identical for every dimension. Ordered low → high. */
export const BANDS: readonly Band[] = Object.freeze([
{ name: 'Prototype', min: 0, max: 19,
meaning: 'Unsafe or broken in ordinary use. Would harm a user or the business if strangers used it today.' },
{ name: 'Alpha', min: 20, max: 39,
meaning: 'Works on the happy path. Known defects that actively harm users or expose the company. Not sellable.' },
{ name: 'Beta', min: 40, max: 59,
meaning: 'Genuinely usable, but ≥1 gap that blocks charging money or inviting scrutiny.' },
{ name: 'Launch-capable, with debt', min: 60, max: 74,
meaning: 'No blocking defect. Real, catalogued debt a competitor or auditor would notice.' },
{ name: 'Production', min: 75, max: 89,
meaning: 'No known blockers. Controls exist AND are verified working. Failures are monitored and recoverable.' },
{ name: 'Mature', min: 90, max: 100,
meaning: 'Independently verified (external audit, pen test, or a real incident survived). Evidence, not assertion.' },
])
export function bandFor(score: number): Band {
const s = Math.max(0, Math.min(100, Math.round(score)))
// Walk high → low so the first match is the highest band the score reaches.
for (let i = BANDS.length - 1; i >= 0; i--) {
const b = BANDS[i]!
if (s >= b.min) return b
}
return BANDS[0]!
}
export type DimensionKey =
| 'security' | 'bugs' | 'compliance' | 'consistency' | 'usability'
export const DIMENSIONS: readonly DimensionKey[] = Object.freeze([
'security', 'bugs', 'compliance', 'consistency', 'usability',
])
export interface CapRule {
/** Stable id so a scorecard can reference which cap bound a score. */
id: string
dimension: DimensionKey
/** Ceiling this defect imposes on its dimension. */
max: number
description: string
}
/** Every hard cap in rubric v1.0, verbatim. */
export const CAP_RULES: readonly CapRule[] = Object.freeze([
// Dimension 1 — SECURITY
{ id: 'sec.cross-tenant', dimension: 'security', max: 30,
description: 'Any live cross-tenant data exposure' },
{ id: 'sec.leaked-secret', dimension: 'security', max: 45,
description: 'Any secret in the repo, in logs, or known-leaked and unrotated' },
{ id: 'sec.no-restore', dimension: 'security', max: 70,
description: 'No verified backup restore' },
{ id: 'sec.no-dep-monitoring', dimension: 'security', max: 75,
description: 'No dependency-vulnerability monitoring' },
// Dimension 2 — BUGS
{ id: 'bugs.data-loss', dimension: 'bugs', max: 35,
description: 'Any known defect causing silent data loss or corruption' },
{ id: 'bugs.takes-money-no-deliver', dimension: 'bugs', max: 40,
description: "Any user-facing flow that takes money and doesn't deliver" },
{ id: 'bugs.no-error-monitoring', dimension: 'bugs', max: 60,
description: 'No error monitoring in production' },
{ id: 'bugs.smoke-only', dimension: 'bugs', max: 70,
description: 'Smoke-tests-only (tests assert files exist, not behavior)' },
// Dimension 3 — COMPLIANCE
{ id: 'comp.false-claim', dimension: 'compliance', max: 50,
description: 'Any live false or unsubstantiated public claim' },
{ id: 'comp.missing-registration', dimension: 'compliance', max: 60,
description: 'Any missing registration that is legally required NOW' },
{ id: 'comp.legal-contradicts-code', dimension: 'compliance', max: 65,
description: 'Legal pages that contradict code behavior' },
// Dimension 4 — CONSISTENCY
{ id: 'cons.per-repo-fix', dimension: 'consistency', max: 65,
description: 'Same critical bug class needing a separate fix per repo' },
{ id: 'cons.docs-contradict', dimension: 'consistency', max: 70,
description: 'Docs/CLAUDE.md materially contradicting the code' },
// Dimension 5 — USABILITY
{ id: 'use.keyboard-inoperable', dimension: 'usability', max: 55,
description: 'Keyboard-inoperable or unlabeled primary flow' },
{ id: 'use.contrast', dimension: 'usability', max: 65,
description: 'Body-copy contrast below AA' },
{ id: 'use.no-empty-error-states', dimension: 'usability', max: 70,
description: 'No empty/error states on primary screens' },
])
const CAPS_BY_ID = new Map(CAP_RULES.map(c => [c.id, c]))
export function capRule(id: string): CapRule | undefined {
return CAPS_BY_ID.get(id)
}
/**
* Score at or above which evidence is mandatory (anti-inflation rule 5:
* "Evidence required above 70").
*/
export const EVIDENCE_THRESHOLD = 70
/** Revenue/exposure weights for the suite average. */
export const TIER_WEIGHTS = Object.freeze({ A: 2, B: 1.5, C: 1 })
export type Tier = keyof typeof TIER_WEIGHTS
+267
View File
@@ -0,0 +1,267 @@
import {
DIMENSIONS,
EVIDENCE_THRESHOLD,
TIER_WEIGHTS,
bandFor,
capRule,
type Band,
type DimensionKey,
type Tier,
} from './rubric.js'
import { type Evidence as SourcedEvidence, evidence as ev } from '@extant2000/evidence-record'
/**
* The original shape: a free-text reference.
*
* @deprecated Pass `@extant2000/evidence-record` evidence instead — `ref` is a
* string a reader has to trust, and this library's own rubric penalises
* unverified claims under "Unverified = absent". Still accepted, and
* converted: `path:42` becomes a file citation, anything else a document one.
*/
export interface Evidence {
/** file:line, a passing test name, a live check, or a production query. */
ref: string
note?: string
}
export type EvidenceLike = Evidence | SourcedEvidence
/** Normalise either shape into a citation the shared renderer can print. */
export function toSourcedEvidence(e: EvidenceLike): SourcedEvidence {
if ('source' in e) return e
const m = /^(.+):(\d+)$/.exec(e.ref)
return m
? ev.file(m[1]!, Number(m[2]), undefined, e.note)
: ev.document(e.ref, undefined, undefined, e.note)
}
/**
* A cap that is currently open, and what establishes that.
*
* The bare-string form is what the first version accepted, and it is exactly
* the gap this release closes: the output said `CAPPED sec.leaked-secret`
* without citing anything, so a reader had no way to check whether the cap
* was still warranted, which is how a cap goes unapplied in the first place.
*/
export type CapClaim = string | { id: string, evidence?: EvidenceLike[] }
export function capId(c: CapClaim): string {
return typeof c === 'string' ? c : c.id
}
function capEvidence(c: CapClaim): SourcedEvidence[] {
return typeof c === 'string' ? [] : (c.evidence ?? []).map(toSourcedEvidence)
}
export interface DimensionAssessment {
/** The scorer's judgement BEFORE caps are applied. */
raw: number
/** Cap rules whose defect is currently open, ideally with what proves it. */
openCaps?: CapClaim[]
/** Required to hold a final score above EVIDENCE_THRESHOLD. */
evidence?: EvidenceLike[]
/** What is holding the score down. Free text, carried through. */
bindingConstraint?: string
/** Single highest-leverage action to raise it. */
nextAction?: string
}
export interface DimensionScore {
dimension: DimensionKey
raw: number
score: number
band: Band
/**
* The cap that actually bound the score, if any — with whatever established
* it, so "CAPPED sec.leaked-secret" is checkable rather than asserted.
*/
boundBy?: { id: string, max: number, description: string, evidence: SourcedEvidence[] }
/** Citations supporting the raw judgement, normalised. */
evidence: SourcedEvidence[]
/** True when the score was reduced to EVIDENCE_THRESHOLD for lack of evidence. */
evidenceWithheld: boolean
bindingConstraint?: string
nextAction?: string
}
/**
* Score one dimension, applying the anti-inflation rules mechanically.
*
* Rule 3 — cap by the WORST defect: the lowest open cap wins. "One serious hole
* is not averaged away by ten strengths."
*
* Rule 5 — evidence required above 70: a score that cannot cite evidence is
* reduced to the threshold rather than rejected, so an un-evidenced assessment
* still produces a usable (and honest) number.
*
* These two are the whole point. Applied by hand, rule 3 is the one that gets
* skipped: a scorer notes an open defect that caps Security at 45 and still
* records 70.
*/
export function scoreDimension(
dimension: DimensionKey,
input: DimensionAssessment,
): DimensionScore {
const raw = clamp(input.raw)
let score = raw
let boundBy: DimensionScore['boundBy']
for (const claim of input.openCaps ?? []) {
const id = capId(claim)
const rule = capRule(id)
if (!rule) throw new Error(`Unknown cap rule: ${id}`)
if (rule.dimension !== dimension) {
throw new Error(`Cap ${id} belongs to ${rule.dimension}, not ${dimension}`)
}
if (rule.max < score) {
score = rule.max
boundBy = {
id: rule.id,
max: rule.max,
description: rule.description,
evidence: capEvidence(claim),
}
}
}
const evidence = (input.evidence ?? []).map(toSourcedEvidence)
const hasEvidence = evidence.length > 0
let evidenceWithheld = false
if (score > EVIDENCE_THRESHOLD && !hasEvidence) {
score = EVIDENCE_THRESHOLD
evidenceWithheld = true
}
return {
dimension,
raw,
score,
band: bandFor(score),
boundBy,
evidence,
evidenceWithheld,
bindingConstraint: input.bindingConstraint,
nextAction: input.nextAction,
}
}
export interface ProductAssessment {
product: string
tier: Tier
dimensions: Record<DimensionKey, DimensionAssessment>
}
export interface ProductScore {
product: string
tier: Tier
weight: number
dimensions: DimensionScore[]
/** The headline number: the LOWEST dimension, not the average. */
overall: number
/** Reported alongside, never instead of, `overall`. */
average: number
band: Band
/** Which dimension set the overall score. */
weakest: DimensionKey
}
/**
* Score one product.
*
* The overall score is the MINIMUM across dimensions — "a product is only as
* shippable as its weakest dimension." The average is reported too, because the
* rubric says to, but it is deliberately not the headline: averaging is how a
* single disqualifying hole disappears behind four healthy numbers.
*/
export function scoreProduct(input: ProductAssessment): ProductScore {
const dimensions = DIMENSIONS.map(d => {
const a = input.dimensions[d]
if (!a) throw new Error(`Missing assessment for dimension: ${d}`)
return scoreDimension(d, a)
})
let weakest = dimensions[0]!
for (const d of dimensions) if (d.score < weakest.score) weakest = d
const overall = weakest.score
const average = round(dimensions.reduce((s, d) => s + d.score, 0) / dimensions.length)
return {
product: input.product,
tier: input.tier,
weight: TIER_WEIGHTS[input.tier],
dimensions,
overall,
average,
band: bandFor(overall),
weakest: weakest.dimension,
}
}
export interface SuiteOptions {
/**
* Cap ids that apply suite-wide (a shared platform flaw hits everything).
* Applied BEFORE averaging, per the rubric's scoring procedure — averaging
* first would let healthy products dilute a flaw that affects them all.
*/
suiteCaps?: CapClaim[]
}
export interface SuiteScore {
products: ProductScore[]
/** Weighted average per dimension, suite caps applied first. */
dimensions: Record<DimensionKey, number>
/** Lowest suite dimension score. The headline. */
overall: number
average: number
band: Band
weakest: DimensionKey
}
export function scoreSuite(
inputs: ProductAssessment[],
options: SuiteOptions = {},
): SuiteScore {
if (inputs.length === 0) throw new Error('scoreSuite requires at least one product')
const products = inputs.map(scoreProduct)
const dimensions = {} as Record<DimensionKey, number>
for (const d of DIMENSIONS) {
let totalWeight = 0
let acc = 0
for (const p of products) {
const ds = p.dimensions.find(x => x.dimension === d)!
// Suite-wide cap applied to each product's score BEFORE it enters the
// weighted mean.
let s = ds.score
for (const claim of options.suiteCaps ?? []) {
const id = capId(claim)
const rule = capRule(id)
if (rule && rule.dimension === d && rule.max < s) s = rule.max
}
acc += s * p.weight
totalWeight += p.weight
}
dimensions[d] = round(acc / totalWeight)
}
let weakest: DimensionKey = DIMENSIONS[0]!
for (const d of DIMENSIONS) if (dimensions[d] < dimensions[weakest]) weakest = d
const overall = dimensions[weakest]
const average = round(
DIMENSIONS.reduce((s, d) => s + dimensions[d], 0) / DIMENSIONS.length,
)
return { products, dimensions, overall, average, band: bandFor(overall), weakest }
}
function clamp(n: number): number {
if (!Number.isFinite(n)) throw new Error(`Score must be a finite number, got ${n}`)
return Math.max(0, Math.min(100, Math.round(n)))
}
function round(n: number): number {
return Math.round(n * 10) / 10
}
+289
View File
@@ -0,0 +1,289 @@
import { describe, expect, it } from 'vitest'
import {
BANDS,
CAP_RULES,
DIMENSIONS,
bandFor,
scoreDimension,
scoreProduct,
scoreSuite,
type DimensionAssessment,
type DimensionKey,
type ProductAssessment,
formatProduct,
productReport,
} from '../src/index.js'
import { validateReport } from '@extant2000/evidence-record'
const ev = [{ ref: 'test/foo.test.ts:12' }]
/** A product scoring `n` on every dimension, with evidence. */
function flat(product: string, n: number, tier: 'A' | 'B' | 'C' = 'B'): ProductAssessment {
const dimensions = {} as Record<DimensionKey, DimensionAssessment>
for (const d of DIMENSIONS) dimensions[d] = { raw: n, evidence: ev }
return { product, tier, dimensions }
}
/** A healthy product with one dimension replaced, for isolating its output. */
function assessmentWith(dim: DimensionKey, a: DimensionAssessment): ProductAssessment {
const p = flat('app', 60)
p.dimensions[dim] = a
return p
}
describe('bandFor', () => {
it('maps the documented boundaries', () => {
expect(bandFor(0).name).toBe('Prototype')
expect(bandFor(19).name).toBe('Prototype')
expect(bandFor(20).name).toBe('Alpha')
expect(bandFor(40).name).toBe('Beta')
expect(bandFor(60).name).toBe('Launch-capable, with debt')
expect(bandFor(75).name).toBe('Production')
expect(bandFor(90).name).toBe('Mature')
expect(bandFor(100).name).toBe('Mature')
})
it('covers 0-100 with no gaps', () => {
for (let s = 0; s <= 100; s++) expect(bandFor(s)).toBeDefined()
})
it('has contiguous non-overlapping bands', () => {
for (let i = 1; i < BANDS.length; i++) {
expect(BANDS[i]!.min).toBe(BANDS[i - 1]!.max + 1)
}
})
})
describe('scoreDimension — anti-inflation rule 3 (cap by worst defect)', () => {
it('CATCHES THE CHARITABLE SCORE: an unrotated leaked secret caps Security at 45', () => {
// A hand scorer can note that a known-leaked, unrotated secret caps the
// dimension at 45 and still record ~70. Charity is not available here.
const r = scoreDimension('security', {
raw: 70,
openCaps: ['sec.leaked-secret'],
evidence: ev,
})
expect(r.raw).toBe(70)
expect(r.score).toBe(45)
expect(r.band.name).toBe('Beta')
expect(r.boundBy?.id).toBe('sec.leaked-secret')
})
it('applies the LOWEST open cap, not the first or last', () => {
const r = scoreDimension('security', {
raw: 90,
openCaps: ['sec.no-dep-monitoring', 'sec.cross-tenant', 'sec.no-restore'],
evidence: ev,
})
expect(r.score).toBe(30)
expect(r.boundBy?.id).toBe('sec.cross-tenant')
})
it('leaves a score below the cap untouched', () => {
const r = scoreDimension('security', {
raw: 25, openCaps: ['sec.leaked-secret'], evidence: ev,
})
expect(r.score).toBe(25)
expect(r.boundBy).toBeUndefined()
})
it('rejects a cap belonging to another dimension', () => {
expect(() => scoreDimension('bugs', { raw: 80, openCaps: ['sec.leaked-secret'] }))
.toThrow(/belongs to security/)
})
it('rejects an unknown cap id rather than ignoring it', () => {
// Silently ignoring a typo'd cap would inflate the score — the exact
// failure mode this library exists to prevent.
expect(() => scoreDimension('security', { raw: 80, openCaps: ['sec.tpyo'] }))
.toThrow(/Unknown cap rule/)
})
})
describe('scoreDimension — anti-inflation rule 5 (evidence above 70)', () => {
it('withholds an un-evidenced score above the threshold', () => {
const r = scoreDimension('bugs', { raw: 85 })
expect(r.score).toBe(70)
expect(r.evidenceWithheld).toBe(true)
})
it('allows an evidenced score above the threshold', () => {
const r = scoreDimension('bugs', { raw: 85, evidence: ev })
expect(r.score).toBe(85)
expect(r.evidenceWithheld).toBe(false)
})
it('does not touch an un-evidenced score at or below the threshold', () => {
const r = scoreDimension('bugs', { raw: 70 })
expect(r.score).toBe(70)
expect(r.evidenceWithheld).toBe(false)
})
it('an empty evidence array is not evidence', () => {
expect(scoreDimension('bugs', { raw: 85, evidence: [] }).score).toBe(70)
})
})
describe('scoreProduct', () => {
it('reports the LOWEST dimension as overall, not the average', () => {
const a = flat('app', 80)
a.dimensions.security = { raw: 40, evidence: ev }
const r = scoreProduct(a)
expect(r.overall).toBe(40)
expect(r.weakest).toBe('security')
expect(r.average).toBe(72) // (40+80*4)/5
expect(r.band.name).toBe('Beta')
})
it('a single capped dimension pins the whole product', () => {
// Four strong dimensions must not average away one disqualifying hole.
const a = flat('api', 88)
a.dimensions.security = { raw: 88, openCaps: ['sec.cross-tenant'], evidence: ev }
const r = scoreProduct(a)
expect(r.overall).toBe(30)
expect(r.band.name).toBe('Alpha')
})
it('assigns the tier weight', () => {
expect(scoreProduct(flat('x', 80, 'A')).weight).toBe(2)
expect(scoreProduct(flat('x', 80, 'B')).weight).toBe(1.5)
expect(scoreProduct(flat('x', 80, 'C')).weight).toBe(1)
})
it('throws on a missing dimension rather than scoring a partial product', () => {
const a = flat('x', 80)
delete (a.dimensions as Partial<Record<DimensionKey, DimensionAssessment>>).usability
expect(() => scoreProduct(a)).toThrow(/Missing assessment/)
})
it('carries the binding constraint and next action through', () => {
const a = flat('x', 80)
a.dimensions.bugs = {
raw: 68, evidence: ev,
bindingConstraint: 'server/api/** handlers untested',
nextAction: 'behaviour-asserting coverage',
}
const bugs = scoreProduct(a).dimensions.find(d => d.dimension === 'bugs')!
expect(bugs.bindingConstraint).toBe('server/api/** handlers untested')
expect(bugs.nextAction).toBe('behaviour-asserting coverage')
})
})
describe('scoreSuite', () => {
it('weights by tier', () => {
// A-tier (2) at 90, C-tier (1) at 60 → (90*2 + 60*1) / 3 = 80
const r = scoreSuite([flat('big', 90, 'A'), flat('small', 60, 'C')])
expect(r.dimensions.security).toBe(80)
})
it('applies a suite-wide cap BEFORE averaging', () => {
// Averaging first would let healthy products dilute a flaw affecting all
// of them: (90+90)/2 = 90, capped after → 90. Capping first → 45.
const r = scoreSuite(
[flat('a', 90, 'A'), flat('b', 90, 'A')],
{ suiteCaps: ['sec.leaked-secret'] },
)
expect(r.dimensions.security).toBe(45)
})
it('leads with the lowest suite dimension', () => {
const a = flat('a', 85)
a.dimensions.consistency = { raw: 63, evidence: ev }
const r = scoreSuite([a])
expect(r.overall).toBe(63)
expect(r.weakest).toBe('consistency')
})
it('throws on an empty suite instead of returning a flattering zero', () => {
expect(() => scoreSuite([])).toThrow(/at least one product/)
})
})
describe('rubric integrity', () => {
it('every cap rule has a unique id', () => {
const ids = CAP_RULES.map(c => c.id)
expect(new Set(ids).size).toBe(ids.length)
})
it('every cap belongs to a known dimension and sits in 0-100', () => {
for (const c of CAP_RULES) {
expect(DIMENSIONS).toContain(c.dimension)
expect(c.max).toBeGreaterThanOrEqual(0)
expect(c.max).toBeLessThanOrEqual(100)
}
})
it('every dimension carries at least one cap', () => {
for (const d of DIMENSIONS) {
expect(CAP_RULES.some(c => c.dimension === d)).toBe(true)
}
})
})
describe('conformance with the evidence-record standard', () => {
const withCapEvidence = scoreDimension('security', {
raw: 70,
openCaps: [{
id: CAP_RULES.find(c => c.dimension === 'security')!.id,
evidence: [{ ref: 'docs/secrets-review.md:12', note: 'exposed key, never rotated' }],
}],
})
it('a cap cites what establishes it', () => {
// The gap this closes: "CAPPED sec.leaked-secret" with nothing behind it.
expect(withCapEvidence.boundBy!.evidence).toHaveLength(1)
const text = formatProduct(scoreProduct(assessmentWith('security', {
raw: 70,
openCaps: [{
id: CAP_RULES.find(c => c.dimension === 'security')!.id,
evidence: [{ ref: 'docs/secrets-review.md:12', note: 'never rotated' }],
}],
})), { evidence: 'full' })
expect(text).toContain('docs/secrets-review.md:12')
expect(text).toContain('never rotated')
})
it('still applies a cap that cites nothing, and says it cited nothing', () => {
// An unproven cap is a smaller error than an unapplied one.
const bare = scoreDimension('security', {
raw: 90,
openCaps: [CAP_RULES.find(c => c.dimension === 'security')!.id],
})
expect(bare.score).toBeLessThan(90)
const text = formatProduct(scoreProduct(assessmentWith('security', {
raw: 90,
openCaps: [CAP_RULES.find(c => c.dimension === 'security')!.id],
})), { evidence: 'full' })
expect(text).toContain('No evidence was recorded for this cap')
})
it('converts a legacy path:line ref into a real citation', () => {
const d = scoreDimension('security', { raw: 50, evidence: [{ ref: 'server/api/x.ts:9' }] })
expect(d.evidence[0]!.source).toMatchObject({ kind: 'file', path: 'server/api/x.ts', line: 9 })
})
it('a score withheld for lack of evidence is not-assessed, not a pass', () => {
const p = scoreProduct(assessmentWith('security', { raw: 95 }))
const f = productReport(p).findings.find(x => x.id.endsWith('.security'))!
expect(f.determination).toBe('not-assessed')
expect(formatProduct(p)).toContain('no evidence was cited')
})
it('every conclusion carries a citation', () => {
expect(validateReport(productReport(scoreProduct(assessmentWith('security', { raw: 60 }))))).toEqual([])
})
it('labels the average so it cannot be read as the headline', () => {
const text = formatProduct(scoreProduct(assessmentWith('security', { raw: 60 })))
expect(text).toContain('never as the headline')
expect(text).toContain('set by the weakest dimension')
})
})
describe('band rendering', () => {
it('prints the band name, not a stringified object', () => {
const text = formatProduct(scoreProduct(flat('app', 80)))
expect(text).not.toContain('[object Object]')
expect(text).toContain('Production')
})
})
+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"]
}