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,8 @@
|
||||
node_modules/
|
||||
dist/
|
||||
*.log
|
||||
.DS_Store
|
||||
.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,13 @@
|
||||
# Changelog
|
||||
|
||||
## 0.3.0 - 2026-09-28
|
||||
|
||||
First public release under MIT.
|
||||
|
||||
- `compareImplementations()` compares a named function across any number of
|
||||
copies, ignoring comments and whitespace, and groups them by behaviour.
|
||||
- A copy where the function cannot be found is a failure, never a pass.
|
||||
- Fewer than two located copies, with none missing, is not assessed.
|
||||
- `toReport()` and `formatDrift()` produce and render an
|
||||
`@extant2000/evidence-record` report that cites every copy.
|
||||
- `extractFunction()`, `codeOnly()` and `fingerprint()` are exported.
|
||||
@@ -0,0 +1,22 @@
|
||||
# Contributing
|
||||
|
||||
Issues and merge requests are welcome at
|
||||
https://gitlab.com/extant2000/blast-radius.
|
||||
|
||||
## Ground rules
|
||||
|
||||
- **A test that cannot fail proves nothing.** If you fix a bug, add a test
|
||||
that fails without your fix, and check that it does fail before you submit.
|
||||
- Measure, do not assume. Two modules with the same line count can be
|
||||
different programs.
|
||||
- Keep dependencies minimal. Every new dependency needs a reason.
|
||||
- Explain why in comments, not what.
|
||||
|
||||
## 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,75 @@
|
||||
# BlastRadius
|
||||
|
||||
Detects when copies of the same function in different codebases drift apart,
|
||||
and names which copies disagree. It is used in Hitt Hosting products.
|
||||
|
||||
## What it does and why
|
||||
|
||||
When the same logic lives in more than one place (two services, a vendored
|
||||
copy, a CommonJS and a TypeScript version), a fix tends to land in only one of
|
||||
them. Each copy looks fine on its own. Only a comparison shows that they no
|
||||
longer agree, and by then the unfixed copy has been running for a while.
|
||||
|
||||
BlastRadius takes the source text of each copy and the name of the function to
|
||||
compare. It extracts the function by brace matching, strips comments and
|
||||
normalises whitespace, fingerprints the result, and groups the copies by
|
||||
fingerprint. More than one group means drift. The largest group comes first,
|
||||
which usually tells you which side has the fix and which side missed it.
|
||||
|
||||
Comments and formatting are ignored because copies may be documented
|
||||
differently but must behave the same. Comparing raw text would fire on a typo
|
||||
fix and train people to update the pinned value without reading, which brings
|
||||
back the drift the check exists to catch. The extraction is not a parser, so
|
||||
it works across CommonJS, ESM and TypeScript without a toolchain per dialect.
|
||||
|
||||
**A copy where the function cannot be found is a failure, not a pass.** It may
|
||||
have been renamed, split or deleted, and a guard that cannot find its subject
|
||||
is not passing. This holds even when only one copy was located.
|
||||
|
||||
Fewer than two located copies, with none missing, is reported as not
|
||||
assessed: one copy cannot disagree with itself, and calling that "in sync"
|
||||
would let a deleted sibling read as a clean result. An in-sync pass cites the
|
||||
copies it compared, because a pass that cannot name what it compared is only
|
||||
asserted.
|
||||
|
||||
The library is pure and has no filesystem access; you read the files. Reports
|
||||
use the `@extant2000/evidence-record` format, and every copy is cited by its
|
||||
path when you give one, or by its label.
|
||||
|
||||
## Usage
|
||||
|
||||
```ts
|
||||
import { compareImplementations, toReport, formatDrift } from '@extant2000/blast-radius'
|
||||
|
||||
const withCheck = 'function canSend(c) {\n if (!c.postalAddress) return false\n return true\n}'
|
||||
const withoutCheck = 'function canSend(c) {\n return true\n}'
|
||||
|
||||
const result = compareImplementations([
|
||||
{ label: 'web', path: 'web/lib/send.ts', source: withCheck },
|
||||
{ label: 'worker', path: 'worker/lib/send.ts', source: withCheck },
|
||||
{ label: 'cli', path: 'cli/lib/send.ts', source: withoutCheck },
|
||||
], 'canSend')
|
||||
|
||||
result.inSync // false
|
||||
result.groups.map(g => g.labels) // [['web', 'worker'], ['cli']]
|
||||
result.missing // []
|
||||
|
||||
const report = toReport(result) // evidence-record report
|
||||
console.log(formatDrift(result, { evidence: 'full' }))
|
||||
```
|
||||
|
||||
The report has one critical finding, "canSend has drifted across 2 variants",
|
||||
citing all three files with the variant each belongs to (2 of 3 and 1 of 3).
|
||||
|
||||
`extractFunction()`, `codeOnly()` and `fingerprint()` (FNV-1a) are exported
|
||||
for use on their own.
|
||||
|
||||
## Install
|
||||
|
||||
```
|
||||
npm install @extant2000/blast-radius
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT. See [LICENSE](LICENSE).
|
||||
@@ -0,0 +1,6 @@
|
||||
# Security
|
||||
|
||||
Please report vulnerabilities privately by email to security@extant2000.com.
|
||||
Do not open a public issue for a security problem.
|
||||
|
||||
We aim to acknowledge every report within 5 business days.
|
||||
Generated
+1504
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "@extant2000/blast-radius",
|
||||
"version": "0.3.0",
|
||||
"private": false,
|
||||
"description": "Shows which systems a change to shared code will affect.",
|
||||
"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/blast-radius.git"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"module": "./dist/index.js",
|
||||
"dependencies": {
|
||||
"@extant2000/evidence-record": "^0.1.2"
|
||||
},
|
||||
"author": "Extant 2000 LLC",
|
||||
"homepage": "https://gitlab.com/extant2000/blast-radius",
|
||||
"bugs": {
|
||||
"url": "https://gitlab.com/extant2000/blast-radius/-/issues"
|
||||
}
|
||||
}
|
||||
+244
@@ -0,0 +1,244 @@
|
||||
/**
|
||||
* Detect when duplicated implementations of the same logic drift apart.
|
||||
*
|
||||
* The failure it guards against: the same logic exists in two places, and a
|
||||
* fix (a compliance check, say) lands in only one of them. Each copy looks
|
||||
* fine on its own; only a comparison shows that they no longer agree.
|
||||
*
|
||||
* Design:
|
||||
* - **Pure.** It takes sources instead of reading files, so it is testable
|
||||
* without a filesystem and usable on any pair of strings.
|
||||
* - **N-way, not a single hash.** This compares any number of copies and
|
||||
* reports WHICH ones diverge — a pinned constant tells you something
|
||||
* moved, not what.
|
||||
*/
|
||||
|
||||
import {
|
||||
type CapabilityReport,
|
||||
type Finding,
|
||||
type FormatOptions,
|
||||
evidence,
|
||||
formatReport as renderReport,
|
||||
} from '@extant2000/evidence-record'
|
||||
|
||||
export interface Implementation {
|
||||
/** How to identify this copy in output, e.g. a repo-relative path. */
|
||||
label: string
|
||||
source: string
|
||||
/**
|
||||
* Filesystem path this copy was read from, when the caller knows it.
|
||||
* Carried into citations so a drift finding points at a file a reader can
|
||||
* open, rather than at a label they have to resolve themselves.
|
||||
*/
|
||||
path?: string
|
||||
}
|
||||
|
||||
export interface DriftGroup {
|
||||
/** Normalised-code fingerprint shared by every member. */
|
||||
fingerprint: string
|
||||
labels: string[]
|
||||
/** Label + path per member, so each copy can be cited individually. */
|
||||
sources: Array<{ label: string, path?: string }>
|
||||
}
|
||||
|
||||
export interface DriftReport {
|
||||
functionName: string
|
||||
/** True when every copy that was found is behaviourally identical. */
|
||||
inSync: boolean
|
||||
/** One entry per distinct implementation. More than one means drift. */
|
||||
groups: DriftGroup[]
|
||||
/** Copies where the function could not be located at all. */
|
||||
missing: string[]
|
||||
/** The same copies with their paths, for citation. */
|
||||
missingSources: Array<{ label: string, path?: string }>
|
||||
/**
|
||||
* True when fewer than two copies were found. Reported separately from
|
||||
* `inSync`: one copy cannot disagree with itself, and calling that "in sync"
|
||||
* would let a deleted or renamed sibling read as a clean result.
|
||||
*/
|
||||
vacuous: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a named function body by brace matching.
|
||||
*
|
||||
* Deliberately not a parser: the point is to work across CommonJS, ESM and
|
||||
* TypeScript copies of the same logic without a toolchain per dialect.
|
||||
*/
|
||||
export function extractFunction(src: string, name: string): string | null {
|
||||
const re = new RegExp(`^(?:export )?(?:async )?function ${name}\\s*\\([^)]*\\)\\s*\\{`, 'm')
|
||||
const m = re.exec(src)
|
||||
if (!m) return null
|
||||
const start = src.indexOf('{', m.index)
|
||||
let depth = 0
|
||||
for (let i = start; i < src.length; i++) {
|
||||
if (src[i] === '{') depth++
|
||||
else if (src[i] === '}') {
|
||||
depth--
|
||||
if (depth === 0) return src.slice(m.index, i + 1)
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip comments and normalise whitespace.
|
||||
*
|
||||
* Comments and formatting are ignored on purpose: two copies are allowed to be
|
||||
* documented differently, but must BEHAVE identically. Comparing raw text would
|
||||
* make the guard fire on a typo fix and train people to bump the hash without
|
||||
* reading — which reintroduces exactly the drift it exists to prevent.
|
||||
*/
|
||||
export function codeOnly(block: string): string {
|
||||
return block
|
||||
.split('\n')
|
||||
.map(l => l.trim())
|
||||
.filter(l => l && !l.startsWith('//'))
|
||||
.map(l => l.replace(/\s*\/\/.*$/, '').replace(/\s+/g, ' '))
|
||||
.filter(Boolean)
|
||||
.join('\n')
|
||||
}
|
||||
|
||||
/** FNV-1a — dependency-free and sufficient for equality grouping. */
|
||||
export function fingerprint(code: string): string {
|
||||
let h = 0x811c9dc5
|
||||
for (let i = 0; i < code.length; i++) {
|
||||
h ^= code.charCodeAt(i)
|
||||
h = Math.imul(h, 0x01000193) >>> 0
|
||||
}
|
||||
return h.toString(16).padStart(8, '0')
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare a named function across several copies.
|
||||
*
|
||||
* `missing` is kept distinct from a drift group. A copy where the function
|
||||
* cannot be found is not "different" — it may have been renamed, split, or
|
||||
* deleted — and silently treating it as absent-therefore-fine is how a guard
|
||||
* stops guarding.
|
||||
*/
|
||||
export function compareImplementations(
|
||||
impls: Implementation[],
|
||||
functionName: string,
|
||||
): DriftReport {
|
||||
const byPrint = new Map<string, Array<{ label: string, path?: string }>>()
|
||||
const missing: string[] = []
|
||||
const missingSources: Array<{ label: string, path?: string }> = []
|
||||
|
||||
for (const impl of impls) {
|
||||
const fn = extractFunction(impl.source, functionName)
|
||||
if (fn == null) {
|
||||
missing.push(impl.label)
|
||||
missingSources.push({ label: impl.label, path: impl.path })
|
||||
continue
|
||||
}
|
||||
const fp = fingerprint(codeOnly(fn))
|
||||
const entry = { label: impl.label, path: impl.path }
|
||||
const list = byPrint.get(fp)
|
||||
if (list) list.push(entry)
|
||||
else byPrint.set(fp, [entry])
|
||||
}
|
||||
|
||||
const groups: DriftGroup[] = [...byPrint.entries()]
|
||||
.map(([fp, sources]) => ({ fingerprint: fp, labels: sources.map(s => s.label), sources }))
|
||||
.sort((a, b) => b.labels.length - a.labels.length)
|
||||
|
||||
const found = groups.reduce((n, g) => n + g.labels.length, 0)
|
||||
|
||||
return {
|
||||
functionName,
|
||||
inSync: groups.length <= 1 && missing.length === 0,
|
||||
groups,
|
||||
missing,
|
||||
missingSources,
|
||||
vacuous: found < 2,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a drift comparison into the portfolio-standard report shape.
|
||||
*
|
||||
* Every copy is cited individually — the path when the caller knows it, the
|
||||
* label otherwise — so a DRIFTED verdict names the files that disagree rather
|
||||
* than asserting that they do.
|
||||
*/
|
||||
export function toReport(report: DriftReport): CapabilityReport {
|
||||
const findings: Finding[] = []
|
||||
const compared = report.groups.reduce((n, g) => n + g.labels.length, 0)
|
||||
|
||||
if (report.missing.length) {
|
||||
findings.push({
|
||||
id: 'missing',
|
||||
summary: `${report.functionName} could not be found in ${report.missing.length} of ${compared + report.missing.length} copies`,
|
||||
determination: 'fail',
|
||||
severity: 'critical',
|
||||
detail: 'Renamed, split, or deleted? A guard that cannot find its subject is not passing.',
|
||||
evidence: report.missingSources.map(s =>
|
||||
s.path
|
||||
? evidence.file(s.path, undefined, undefined, `${report.functionName} not found`)
|
||||
: evidence.document(s.label, undefined, undefined, `${report.functionName} not found`)),
|
||||
})
|
||||
}
|
||||
|
||||
if (report.groups.length > 1) {
|
||||
findings.push({
|
||||
id: 'drift',
|
||||
summary: `${report.functionName} has drifted across ${report.groups.length} variants`,
|
||||
determination: 'fail',
|
||||
severity: 'critical',
|
||||
detail: 'Apply the same change to every copy — do not bump a pinned hash on one side.',
|
||||
// Majority variant first: the sort in compareImplementations already
|
||||
// puts it there, and naming it first tells a reader which side is
|
||||
// probably the fix and which is probably the miss.
|
||||
evidence: report.groups.flatMap(g => g.sources.map(s =>
|
||||
s.path
|
||||
? evidence.file(s.path, undefined, undefined, `variant ${g.fingerprint} (${g.labels.length} of ${compared})`)
|
||||
: evidence.document(s.label, undefined, undefined, `variant ${g.fingerprint} (${g.labels.length} of ${compared})`))),
|
||||
})
|
||||
}
|
||||
|
||||
if (report.inSync && !report.vacuous) {
|
||||
const g = report.groups[0]!
|
||||
findings.push({
|
||||
id: 'in-sync',
|
||||
summary: `${report.functionName}: ${g.labels.length} copies identical`,
|
||||
determination: 'pass',
|
||||
severity: 'info',
|
||||
evidence: g.sources.map(s =>
|
||||
s.path
|
||||
? evidence.file(s.path, undefined, undefined, `fingerprint ${g.fingerprint}`)
|
||||
: evidence.document(s.label, undefined, undefined, `fingerprint ${g.fingerprint}`)),
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
capability: 'BlastRadius',
|
||||
scope: `${report.functionName} across ${compared + report.missing.length} copies`,
|
||||
// Fewer than two LOCATED copies means nothing was compared, which drives
|
||||
// the report to NOT ASSESSED without a special case. One copy cannot
|
||||
// disagree with itself, and calling that "in sync" would let a deleted
|
||||
// sibling read as a clean result.
|
||||
//
|
||||
// But `vacuous` counts located copies only, so one copy plus one where
|
||||
// the function is MISSING is vacuous by that definition — and a missing
|
||||
// subject is a finding, not an absence of one. It stays assessed.
|
||||
examined: report.vacuous && report.missing.length === 0
|
||||
? 0
|
||||
: compared + report.missing.length,
|
||||
findings,
|
||||
notes: report.vacuous && report.missing.length === 0
|
||||
? ['Fewer than two copies were found, so nothing was compared. One copy cannot disagree with itself.']
|
||||
: [],
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a drift report for a CI log.
|
||||
*
|
||||
* Delegates to the shared renderer. What used to print VACUOUS now prints
|
||||
* NOT ASSESSED plus the sentence explaining it — same distinction, no
|
||||
* glossary.
|
||||
*/
|
||||
export function formatDrift(report: DriftReport, opts: FormatOptions = {}): string {
|
||||
return renderReport(toReport(report), opts)
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { overall, validateReport } from '@extant2000/evidence-record'
|
||||
import {
|
||||
codeOnly,
|
||||
toReport,
|
||||
compareImplementations,
|
||||
extractFunction,
|
||||
fingerprint,
|
||||
formatDrift,
|
||||
} from '../src/index.js'
|
||||
|
||||
const A = `
|
||||
// header comment
|
||||
function evaluateSendRules(c) {
|
||||
if (!c.postalAddress) return { ok: false }
|
||||
return { ok: true }
|
||||
}
|
||||
module.exports = { evaluateSendRules }
|
||||
`
|
||||
|
||||
// Same behaviour, different comments and formatting — must NOT be drift.
|
||||
const B = `
|
||||
/* different doc */
|
||||
function evaluateSendRules(c) {
|
||||
// a comment the other copy does not have
|
||||
if (!c.postalAddress) return { ok: false }
|
||||
|
||||
return { ok: true } // trailing note
|
||||
}
|
||||
`
|
||||
|
||||
// A real behavioural change — the postal-address check is gone.
|
||||
const C = `
|
||||
function evaluateSendRules(c) {
|
||||
return { ok: true }
|
||||
}
|
||||
`
|
||||
|
||||
describe('extractFunction', () => {
|
||||
it('extracts a function by brace matching', () => {
|
||||
expect(extractFunction(A, 'evaluateSendRules')).toContain('postalAddress')
|
||||
})
|
||||
|
||||
it('handles nested braces without stopping early', () => {
|
||||
const src = 'function f() { if (x) { g({ a: 1 }) } return 2 }'
|
||||
expect(extractFunction(src, 'f')).toBe(src)
|
||||
})
|
||||
|
||||
it('finds async and exported declarations', () => {
|
||||
expect(extractFunction('async function f() { return 1 }', 'f')).toBeTruthy()
|
||||
expect(extractFunction('export function f() { return 1 }', 'f')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('returns null when the function is absent', () => {
|
||||
expect(extractFunction(A, 'nope')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('codeOnly', () => {
|
||||
it('ignores comments and whitespace so docs may differ', () => {
|
||||
expect(codeOnly(extractFunction(A, 'evaluateSendRules')!))
|
||||
.toBe(codeOnly(extractFunction(B, 'evaluateSendRules')!))
|
||||
})
|
||||
|
||||
it('does NOT ignore a behavioural change', () => {
|
||||
expect(codeOnly(extractFunction(A, 'evaluateSendRules')!))
|
||||
.not.toBe(codeOnly(extractFunction(C, 'evaluateSendRules')!))
|
||||
})
|
||||
})
|
||||
|
||||
describe('compareImplementations', () => {
|
||||
it('reports identical copies as in sync', () => {
|
||||
const r = compareImplementations(
|
||||
[{ label: 'a', source: A }, { label: 'b', source: B }],
|
||||
'evaluateSendRules',
|
||||
)
|
||||
expect(r.inSync).toBe(true)
|
||||
expect(r.groups).toHaveLength(1)
|
||||
expect(r.vacuous).toBe(false)
|
||||
})
|
||||
|
||||
it('catches the one-sided fix — the failure this exists for', () => {
|
||||
// A CAN-SPAM guard removed in one repo only.
|
||||
const r = compareImplementations(
|
||||
[{ label: 'hosting', source: A }, { label: 'crm', source: C }],
|
||||
'evaluateSendRules',
|
||||
)
|
||||
expect(r.inSync).toBe(false)
|
||||
expect(r.groups).toHaveLength(2)
|
||||
expect(formatDrift(r)).toContain('has drifted across')
|
||||
})
|
||||
|
||||
it('groups three copies as 2-vs-1 and names which side is odd', () => {
|
||||
const r = compareImplementations(
|
||||
[{ label: 'a', source: A }, { label: 'b', source: B }, { label: 'c', source: C }],
|
||||
'evaluateSendRules',
|
||||
)
|
||||
// Majority group first.
|
||||
expect(r.groups[0]!.labels).toEqual(['a', 'b'])
|
||||
expect(r.groups[1]!.labels).toEqual(['c'])
|
||||
})
|
||||
|
||||
it('treats a MISSING function as not-found, never as passing', () => {
|
||||
// Renamed or deleted is not "no drift" — a guard that cannot find its
|
||||
// subject is not passing.
|
||||
const r = compareImplementations(
|
||||
[{ label: 'a', source: A }, { label: 'gone', source: 'function other() {}' }],
|
||||
'evaluateSendRules',
|
||||
)
|
||||
expect(r.missing).toEqual(['gone'])
|
||||
expect(r.inSync).toBe(false)
|
||||
expect(formatDrift(r)).toContain('could not be found')
|
||||
})
|
||||
|
||||
it('flags a single copy as NOT ASSESSED, not in sync', () => {
|
||||
// One copy cannot disagree with itself; calling that a pass would let a
|
||||
// deleted sibling read as clean.
|
||||
const r = compareImplementations([{ label: 'a', source: A }], 'evaluateSendRules')
|
||||
expect(r.vacuous).toBe(true)
|
||||
const text = formatDrift(r)
|
||||
expect(text).toContain('NOT ASSESSED')
|
||||
expect(text).toContain('One copy cannot disagree with itself')
|
||||
// The word it replaced needed a glossary.
|
||||
expect(text).not.toContain('VACUOUS')
|
||||
})
|
||||
|
||||
it('an empty input set is vacuous', () => {
|
||||
expect(compareImplementations([], 'f').vacuous).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('fingerprint', () => {
|
||||
it('is stable and differs on changed code', () => {
|
||||
expect(fingerprint('abc')).toBe(fingerprint('abc'))
|
||||
expect(fingerprint('abc')).not.toBe(fingerprint('abd'))
|
||||
})
|
||||
})
|
||||
|
||||
describe('conformance with the evidence-record standard', () => {
|
||||
const A = 'function evaluateSendRules(c) {\n if (c.unsub) return false\n return true\n}'
|
||||
const B = 'function evaluateSendRules(c) {\n return true\n}'
|
||||
|
||||
it('cites the file of every copy that disagrees', () => {
|
||||
const r = compareImplementations([
|
||||
{ label: 'crm', path: 'service-a/lib/send.js', source: A },
|
||||
{ label: 'hosting', path: 'service-b/lib/send.js', source: B },
|
||||
], 'evaluateSendRules')
|
||||
const text = formatDrift(r, { evidence: 'full' })
|
||||
expect(text).toContain('service-a/lib/send.js')
|
||||
expect(text).toContain('service-b/lib/send.js')
|
||||
expect(overall(toReport(r))).toBe('fail')
|
||||
})
|
||||
|
||||
it('cites the copies behind an in-sync pass', () => {
|
||||
// A pass that cannot name what it compared is an asserted result.
|
||||
const r = compareImplementations([
|
||||
{ label: 'crm', path: 'a/send.js', source: A },
|
||||
{ label: 'hosting', path: 'b/send.js', source: A },
|
||||
], 'evaluateSendRules')
|
||||
expect(overall(toReport(r))).toBe('pass')
|
||||
expect(formatDrift(r, { evidence: 'full' })).toContain('a/send.js')
|
||||
})
|
||||
|
||||
it('every conclusion carries a citation', () => {
|
||||
const r = compareImplementations([
|
||||
{ label: 'crm', path: 'a/send.js', source: A },
|
||||
{ label: 'hosting', path: 'b/send.js', source: B },
|
||||
], 'evaluateSendRules')
|
||||
expect(validateReport(toReport(r))).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('a missing subject is a finding, not an absence of one', () => {
|
||||
it('stays assessed when one copy is present and one is missing', () => {
|
||||
// `vacuous` counts LOCATED copies, so this pair is vacuous by that
|
||||
// definition. Reporting it as NOT ASSESSED would hide a deleted or
|
||||
// renamed sibling behind "nothing was compared" — the exact failure this
|
||||
// library exists to catch.
|
||||
const r = compareImplementations(
|
||||
[
|
||||
{ label: 'a', path: 'a/send.js', source: 'function evaluateSendRules(c) { return true }' },
|
||||
{ label: 'gone', path: 'b/send.js', source: 'function other() {}' },
|
||||
],
|
||||
'evaluateSendRules',
|
||||
)
|
||||
expect(r.vacuous).toBe(true)
|
||||
expect(overall(toReport(r))).toBe('fail')
|
||||
expect(formatDrift(r)).toContain('could not be found')
|
||||
expect(formatDrift(r)).not.toContain('NOT ASSESSED')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "CommonJS",
|
||||
"moduleResolution": "Node",
|
||||
"outDir": "dist-cjs",
|
||||
"declaration": false
|
||||
}
|
||||
}
|
||||
@@ -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