First public release
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
@@ -0,0 +1,9 @@
|
||||
node_modules/
|
||||
dist/
|
||||
*.log
|
||||
.DS_Store
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
.npmrc
|
||||
dist-cjs/
|
||||
@@ -0,0 +1,6 @@
|
||||
stages: [test]
|
||||
|
||||
test:
|
||||
stage: test
|
||||
image: node:22-alpine
|
||||
script: [npm ci, npm run build, npm test]
|
||||
@@ -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`.
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
@@ -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.
|
||||
@@ -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.')
|
||||
}
|
||||
Generated
+1504
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './rubric.js'
|
||||
export * from './score.js'
|
||||
export * from './report.js'
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022", "module": "ES2022", "moduleResolution": "bundler",
|
||||
"declaration": true, "outDir": "dist", "rootDir": "src",
|
||||
"strict": true, "skipLibCheck": true
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user