commit e8af6d7e94b559fe3cf46d6722aec323a8381f0c Author: Paul Hitt Date: Mon Sep 28 14:45:34 2026 -0400 First public release Co-Authored-By: Claude Opus 5.5 diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..f15441a --- /dev/null +++ b/.editorconfig @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5fd8e1d --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +node_modules/ +dist/ +*.log +.DS_Store +.env* +!.env.example +.npmrc +dist-cjs/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..e5a37d2 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,6 @@ +stages: [test] + +test: + stage: test + image: node:22-alpine + script: [npm ci, npm run build, npm test] diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..c4e30e9 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,18 @@ +# Changelog + +## 0.1.0 - 2026-09-28 + +First public release under MIT. + +- `synthesize()` and `formatSynthesis()` check a mission concept's + requirements, architecture options and TRL claim. +- Derivation is checked both ways: invented requirements, unaddressed needs + and parent ids that do not exist. +- Requirements with no verification method, or an undecidable criterion, + fail. +- The claimed TRL is capped by the strongest recorded evidence, and no + evidence returns `null`, not TRL 1. `maturationPath()` runs from the + effective level. +- Trade-study checks: a single option is not assessed, options identical on + every criterion fail, flat criteria and dominated options are noted, and + requirements no option satisfies fail. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..8b4634b --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,22 @@ +# Contributing + +Issues and merge requests are welcome at +https://gitlab.com/extant2000/mission-synth. + +## 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 +``` diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..34e49b1 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..7d8a784 --- /dev/null +++ b/README.md @@ -0,0 +1,144 @@ +# MissionSynth + +Checks that a mission concept holds together: requirements traced to needs in +both directions, verification criteria someone can decide, architecture +options that differ, and a TRL claim capped by evidence. + +## What it does and why + +MissionSynth does not write requirements. Turning a concept into candidate +requirements and architectures is generative work, done by people or by a +model. What decides whether that output survives a review is whether it holds +together, and that part can be checked: + +- every requirement traces to a stated need, and every need to a requirement; +- every requirement names a verification method and a criterion someone could + decide; +- the architecture options differ on at least one stated criterion; +- the claimed TRL does not exceed what the evidence demonstrates. + +It is pure: no clock, no I/O and no model calls. Reports use the +`@extant2000/evidence-record` format. The requirement id is the key a +downstream traceability tool can pick up. + +## Derivation, checked both ways + +The two defects a requirement set tends to have are opposites, and a check in +one direction finds only one of them. + +| Defect | Typical of | +|---|---| +| Invented: traces to no stated need | A generated set. Producing plausible requirements is easy; producing only warranted ones is not. | +| Unaddressed need: no requirement covers it | A hand-written set. The needs that get forgotten are the ones nobody enjoys thinking about. | + +A parent id that does not exist (`dangling-parent`) is its own defect. It is +the same as no parent, and it looks like traceability. + +A requirement with no verification method fails, and so does one whose +criterion cannot be decided. A requirement nobody can fail passes every +review. `isMeasurable()` is a smell test, not a grammar. It catches the common +cases ("user-friendly", "acceptable", "intuitive"), but a criterion can be +unmeasurable without using any of those words, so a clean result does not +prove the criterion is good. + +## TRL, capped by evidence + +Technology Readiness Level is usually self-assessed, and it only goes wrong in +one direction: nobody claims a level below what they can show. The +definitions are specific, which is what makes this checkable. Each level names +the environment something was demonstrated in, and an environment is a fact. + +``` +TRL 4 component validated in a laboratory +TRL 5 component validated in a relevant environment +TRL 6 system or subsystem model demonstrated in a relevant environment +TRL 7 system prototype demonstrated in an operational environment +``` + +`EVIDENCE_CEILING` maps each kind of demonstration to the highest level it can +support. A bench test or a unit test (`lab-bench`) supports TRL 3, so a TRL 6 +claim backed by one is capped at 3. The best evidence sets the ceiling: +having also written a paper does not drag down a system demonstration. No +evidence at all returns `null`, not TRL 1, because TRL 1 is itself a claim +that principles were observed and reported. + +`maturationPath()` runs from the effective level, not the claimed one, and +each step names the demonstration that raises the number. A roadmap whose +steps produce no evidence is a schedule. + +## A trade study with nothing to trade + +- **One option** is a decision already made. It is reported as not assessed, + whatever the document is called. +- Options identical on every criterion fail at critical severity. The table is + full, the columns are populated, and the choice rests on something not + written down, which is why this passes a quick review. +- Criteria that are the same across all options are named as doing no work in + the decision. That is a note, not a fault. +- An option that satisfies a strict subset of another's requirements and adds + nothing of its own is flagged as dominated. It may still win on cost, but + carrying it unexamined makes the study look broader than it is. +- A requirement that no option satisfies fails. + +## Usage + +```ts +import { synthesize, formatSynthesis, type MissionConcept } from '@extant2000/mission-synth' + +const concept: MissionConcept = { + name: 'deorbit module', + needs: [ + { id: 'N-1', statement: 'deorbit within 25 years of end of mission', + source: 'debris mitigation guideline', where: 'concept/needs.md' }, + { id: 'N-2', statement: 'survive launch loads', source: 'launch provider', where: 'concept/needs.md' }, + ], + requirements: [ + { id: 'SYS-001', statement: 'The system shall deorbit within 25 years', derivedFrom: ['N-1'], + verification: { method: 'analysis', criterion: 're-entry at less than 25 years' }, + where: 'concept/requirements.md' }, + { id: 'SYS-002', statement: 'The system shall be easy to integrate', derivedFrom: [], + verification: { method: 'inspection', criterion: 'integration is intuitive' }, + where: 'concept/requirements.md' }, + ], + options: [ + { id: 'A', name: 'drag sail', satisfies: ['SYS-001'], criteria: { mass: 'low', cost: 2 }, where: 'trade/a.md' }, + { id: 'B', name: 'propulsive', satisfies: ['SYS-001'], criteria: { mass: 'low', cost: 2 }, where: 'trade/b.md' }, + ], + claimedTrl: 6, + targetTrl: 7, + maturity: [{ evidence: 'lab-bench', description: 'deployment mechanism cycled on the bench', where: 'test/TR-12' }], +} + +const report = synthesize(concept) +console.log(formatSynthesis(concept)) +``` + +The finding lines from that output (header, notes and detail lines omitted): + +``` +✗ CRITICAL 1 requirement(s) satisfied by no option: SYS-002 [NOT MET] +✗ CRITICAL No criterion tells the 2 options apart [NOT MET] +✗ CRITICAL N-2 is stated and no requirement covers it [NOT MET] +✗ CRITICAL SYS-002: invented [NOT MET] +✗ CRITICAL TRL 6 claimed, evidence supports TRL 3 [NOT MET] +✗ HIGH SYS-002: immeasurable [NOT MET] +◦ info SYS-001 traces to N-1 and is verifiable by analysis [MET] +``` + +The notes also give the maturation path to TRL 7: four steps starting from +the effective TRL 3, not one step from the claimed TRL 6. + +The building blocks are exported too: `requirementIssues()`, +`unaddressedNeeds()`, `isMeasurable()`, `nonDiscriminating()`, +`isUndiscriminated()`, `dominatedOptions()`, `uncoveredRequirements()`, +`supportedTrl()`, `assessTrl()` and `maturationPath()`. + +## Install + +``` +npm install @extant2000/mission-synth +``` + +## License + +MIT. See [LICENSE](LICENSE). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..b88987e --- /dev/null +++ b/SECURITY.md @@ -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. diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..5675a9d --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1503 @@ +{ + "name": "@extant2000/mission-synth", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@extant2000/mission-synth", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "@extant2000/evidence-record": "^0.1.2" + }, + "devDependencies": { + "typescript": "^5.6.0", + "vitest": "^2.1.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@extant2000/evidence-record": { + "version": "0.1.2" + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.5.tgz", + "integrity": "sha512-J25QJU+B78T4FhhBsNpLJyVWOi31mwtpcMwywHmOKH65Q9IWGA81gPj+dnwlhU8wktVriYE+tFAaQgrnJRzAZg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.5.tgz", + "integrity": "sha512-LDopB3zuZM5Ux9TT2luNEBJW/tYbGU2g1d+VpKk6I+gSKDb+/7sYE6M225gRQt4RbMX6MSwMsVR/phdjVUgRLg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.5.tgz", + "integrity": "sha512-wlJEERGfeuHeBavCL2qVnNacOK43NDoZM4sjkeRPymd04OAE9T1zBqDJgmZ+CIsPTYKwdzpUC8vmOw84dwY4Tg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.5.tgz", + "integrity": "sha512-4nJJGg5jbo2wwPP4JP+LfEBA3bvP8rU9CLuhp7jWvq9sxEyhjQFTFdrqi+/dHEin/pd8jpT0vcehIpnZtmEdcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.5.tgz", + "integrity": "sha512-DrZbyCDF1hneuO6jRbvZ2D7+PIBM6yIwYnJpg2vIk58T+wuFpiaGZrfUr59lDWw45bg+IrpTGLPiNi/Fk4w3Cg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.5.tgz", + "integrity": "sha512-gqfUVMJMB3mehqywxp6hTBFfgtMQykZY19+cfiaYP0toIJLb/1DZRJHVkQQGP13W4TAwfZDWeg1qBcheTRioXQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.5.tgz", + "integrity": "sha512-CFmhpvAwzSaWMlN3VN7UtmoTihlZNzoP0juQib5TQRnYUyDV8dXeWOp29sobWAT6gXl/hQgAClLlEiYozQG3OQ==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.5.tgz", + "integrity": "sha512-Uc9H8eXCOayV6JLTH5bXKMId6qbhNHa818/BgYjm4jrlq3vZquC9cqyvHBw17xy5Mnj5f+I3gFK5JcEf3hSqrw==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.5.tgz", + "integrity": "sha512-VcPr/szv/1BFw112Kt//fxulXt/JPqzzidU84iW68L2DdjnOO8QFUv2zTSYBEPHD6movBD4z+bbr5y60GYM7Jw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.5.tgz", + "integrity": "sha512-BnxtJ5/91BrIHYIkGrmjz/lbMhqEHt1dPFqIxIFR+jPn0xVc/oUSCtIT089zfp5ufwGDlYz2UC+Fe1SRBpYFbQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.5.tgz", + "integrity": "sha512-LrYcHZwF+fAMNKHYTOQ5osWM4AZF7YF6D+XtsjDyEvljtt11twc+zHVXBLNEjxVSUnKYsOhvVz4Z213eW02COQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.5.tgz", + "integrity": "sha512-nj7QKQePAAUpCpJHtg0pR0W/b92A9NO17JS3BAQmHDn/yhmkir2p8llrKY9TOhleKIaSzy1JhxS3T9FVld6coA==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.5.tgz", + "integrity": "sha512-5ylkX6dWMeBKge9nTU+Rxfb+ZfaCIJ9lRqIFaK0eAMcWp7OJbYnLveLgXmm0VrvuLKb8qIK+mHyH0qu88RM+iA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.5.tgz", + "integrity": "sha512-oHK4ZHYFDKjZviK34I+NwgfbGxgI7ztrNxj2hPTSSNFgeq1a/lEd7dHV2fdGAuTH4Iym3RHJg+vAbWaWG4B7Zg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.5.tgz", + "integrity": "sha512-UcetmHZ6XOXuUByiKZyQmb55ZPr0LABr3Ec/HB9wKZn6CEAFWZkE+hsJErJ9hbPBC7nI0dKuELx7CoV6IM7TMg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.5.tgz", + "integrity": "sha512-C5CmDPQBtvjVo8cgQsBs+w6WB0JLkiixhgi6hVLV11hERWdn/p0XcPU2OUcZzac9BPOFq7SbaHFa8r3SWEysCQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.5.tgz", + "integrity": "sha512-lHVQHJFKsuuxLMi3MQO9XVL8Tje3JR82CzB+QDKC5NWBcsIWuwsn9uIM5e3lBhI+fF1/s63qnyYqsg65+8rV/w==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.5.tgz", + "integrity": "sha512-3W9bTFcQNJn71cSJVM9RKIiZOy8DO/XLDii8Uv/Pm6WKqDRj7JV3ZfuXIEfyuy5LXpIzAbB/1M4Ukp9GKNa7nA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.5.tgz", + "integrity": "sha512-VDC7rRJlee/scpki96GZ27Omf6yU87s1YXwVTpjE5841faVlDYYT565rgfmoR1U0sqL7z5ivQSDjcsF6VRXyBA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.5.tgz", + "integrity": "sha512-z86Ok2p4pTdv5xqCKZsTooO7yBEiaJR/HzU3Wx8RmWsPoLppnMKROhJusQob8B3IE1ghC343kUW9rC2r+Wf3ig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.5.tgz", + "integrity": "sha512-IzQmj+xXwQFGhMAMKMQVXkMwMZN3TqkJgAE0nSsqvVwWWciP4AIPMmWRqOQ2GfX7TUDZr+xqGFcBS36CRPGw0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.5.tgz", + "integrity": "sha512-F6qpTaPc9bwBH85kjy0/BLmLSW1uv7AoOXCoRIkg2arlgCYlWYcAbiMkvZuAcaWk9TpCRG//okznLAqLGshkMw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.5.tgz", + "integrity": "sha512-igoDsTFhhwECBeGbUuLeIk7t8Y1apa+cs6mDWpx2EZ0ch7oEQgzHbFUXN9euoHekCAQzXdXApAGkV6jznS7tWw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.5.tgz", + "integrity": "sha512-U3teMeMbXFmaM5D+OTJpsOXd+wV/qftIeYF9kBKL4v73641qyJmoXFtA28DQLsnmlyayEsTe72xpLHrArq6vHw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.5.tgz", + "integrity": "sha512-ypfC34F3RKXvCXBglGqGMsUSMKlgwd1HX9AOAlx9RoZZ6GaI42YHVeKpzg3JG+wpBUJYTG+NNZhqbDWL8tBZkw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.63.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.5.tgz", + "integrity": "sha512-KRWwmNLlPw5M7HcdYfm15oBv9n9LPtjzpzCIxS/phwqvPyxHSoKX6Y2YU3pxSPfy0CLquVgsx/j/hBi6OvH1Nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.63.5", + "@rollup/rollup-android-arm64": "4.63.5", + "@rollup/rollup-darwin-arm64": "4.63.5", + "@rollup/rollup-darwin-x64": "4.63.5", + "@rollup/rollup-freebsd-arm64": "4.63.5", + "@rollup/rollup-freebsd-x64": "4.63.5", + "@rollup/rollup-linux-arm-gnueabihf": "4.63.5", + "@rollup/rollup-linux-arm-musleabihf": "4.63.5", + "@rollup/rollup-linux-arm64-gnu": "4.63.5", + "@rollup/rollup-linux-arm64-musl": "4.63.5", + "@rollup/rollup-linux-loong64-gnu": "4.63.5", + "@rollup/rollup-linux-loong64-musl": "4.63.5", + "@rollup/rollup-linux-ppc64-gnu": "4.63.5", + "@rollup/rollup-linux-ppc64-musl": "4.63.5", + "@rollup/rollup-linux-riscv64-gnu": "4.63.5", + "@rollup/rollup-linux-riscv64-musl": "4.63.5", + "@rollup/rollup-linux-s390x-gnu": "4.63.5", + "@rollup/rollup-linux-x64-gnu": "4.63.5", + "@rollup/rollup-linux-x64-musl": "4.63.5", + "@rollup/rollup-openbsd-x64": "4.63.5", + "@rollup/rollup-openharmony-arm64": "4.63.5", + "@rollup/rollup-win32-arm64-msvc": "4.63.5", + "@rollup/rollup-win32-ia32-msvc": "4.63.5", + "@rollup/rollup-win32-x64-gnu": "4.63.5", + "@rollup/rollup-win32-x64-msvc": "4.63.5", + "fsevents": "~2.3.2" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..253ff45 --- /dev/null +++ b/package.json @@ -0,0 +1,46 @@ +{ + "name": "@extant2000/mission-synth", + "version": "0.1.0", + "private": false, + "description": "Checks a mission concept: requirement traceability both ways, decidable verification, discriminated options and a TRL capped by evidence.", + "license": "MIT", + "type": "module", + "main": "./dist-cjs/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist", + "dist-cjs", + "src", + "README.md", + "LICENSE" + ], + "repository": { + "type": "git", + "url": "https://gitlab.com/extant2000/mission-synth.git" + }, + "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" + }, + "dependencies": { + "@extant2000/evidence-record": "^0.1.2" + }, + "devDependencies": { + "typescript": "^5.6.0", + "vitest": "^2.1.0" + }, + "author": "Extant 2000 LLC", + "homepage": "https://gitlab.com/extant2000/mission-synth", + "bugs": { + "url": "https://gitlab.com/extant2000/mission-synth/-/issues" + } +} diff --git a/src/architecture.ts b/src/architecture.ts new file mode 100644 index 0000000..2cbe8f8 --- /dev/null +++ b/src/architecture.ts @@ -0,0 +1,97 @@ +/** + * Architecture options, checked for whether they are actually options. + * + * A trade study is theatre in two recognisable ways, and both survive review + * because the document looks right: + * + * 1. **One option.** A single architecture presented with a rationale is a + * decision that has already been made. There is nothing to trade. + * + * 2. **Options that do not differ.** Two or three alternatives that score + * identically on every stated criterion. The table is full, the columns + * are populated, and no criterion discriminates — so the choice between + * them rests on something not written down. That is the case worth + * catching, because it is indistinguishable from real analysis at a + * glance. + * + * Separately: an option that satisfies fewer requirements than another is + * not automatically worse — it may be cheaper or simpler on a criterion that + * matters more. But an option satisfying NO requirement the others do not, + * while satisfying strictly fewer, is dominated, and carrying it makes the + * study look broader than it is. + */ + +export interface ArchitectureOption { + id: string + name: string + /** Requirement ids this option satisfies. */ + satisfies: string[] + /** + * Scores or values per criterion, keyed by criterion name. Numbers or + * short labels — whatever the study actually recorded. + */ + criteria: Record + where: string +} + +/** Criteria on which every option holds the same value. */ +export function nonDiscriminating(options: ArchitectureOption[]): string[] { + const list = options ?? [] + if (list.length < 2) return [] + + const names = [...new Set(list.flatMap(o => Object.keys(o.criteria)))] + return names.filter(name => { + const values = list.map(o => String(o.criteria[name] ?? '∅')) + return new Set(values).size === 1 + }).sort() +} + +/** True when no criterion tells any two options apart. */ +export function isUndiscriminated(options: ArchitectureOption[]): boolean { + const list = options ?? [] + if (list.length < 2) return false + const names = [...new Set(list.flatMap(o => Object.keys(o.criteria)))] + if (names.length === 0) return true + return nonDiscriminating(list).length === names.length +} + +export interface Domination { + dominated: ArchitectureOption + by: ArchitectureOption +} + +/** + * Options strictly beaten on requirement coverage by another. + * + * Coverage only — this deliberately does not judge the criteria, because a + * thinner option can be the right answer on cost or schedule. It flags the + * case where an option satisfies a strict subset of another's requirements + * AND adds nothing of its own. + */ +export function dominatedOptions(options: ArchitectureOption[]): Domination[] { + const list = options ?? [] + const out: Domination[] = [] + + for (const a of list) { + for (const b of list) { + if (a.id === b.id) continue + const aSet = new Set(a.satisfies) + const bSet = new Set(b.satisfies) + const aUnique = [...aSet].filter(r => !bSet.has(r)) + if (aUnique.length === 0 && bSet.size > aSet.size) { + out.push({ dominated: a, by: b }) + break + } + } + } + return out +} + +/** Requirements no option satisfies. */ +export function uncoveredRequirements( + options: ArchitectureOption[], + requirementIds: string[], +): string[] { + const covered = new Set((options ?? []).flatMap(o => o.satisfies)) + return (requirementIds ?? []).filter(id => !covered.has(id)) +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..2c458d8 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,273 @@ +/** + * MissionSynth — turn a mission concept into defensible requirements, + * discriminated architecture options, and a TRL roadmap capped by evidence. + * + * **What this does and does not do.** It does not write requirements. Turning + * a concept into candidate requirements and architectures is generative + * work, done by people or by a model, and a pure library cannot do it + * honestly. What a library CAN do — and what decides whether the output + * survives a review — is check that the synthesis holds together: + * + * - every requirement traces to a stated need, and every need to a + * requirement (the two failures are opposites, so both directions run) + * - every requirement names a verification method and a criterion someone + * could decide + * - the architecture options actually differ on a stated criterion + * - the TRL claim does not exceed what the evidence demonstrates + * + * That is the same discipline as the rest of this set: AdversarialSuite + * derives cases and runs nothing, SelfServe gates provisioning and provisions + * nothing, and this structures a synthesis it does not perform. + * + * It sits UPSTREAM of `@extant2000/trace-proof`, which takes the requirements + * that survive this and links them to system evidence. The handover is the + * requirement id. + * + * Pure: no clock, no I/O, no model calls. + */ + +import { + type CapabilityReport, + type Finding, + type FormatOptions, + evidence, + formatReport as renderReport, +} from '@extant2000/evidence-record' + +import { + type Need, + type Requirement, + requirementIssues, + unaddressedNeeds, +} from './requirements.js' +import { + type ArchitectureOption, + dominatedOptions, + isUndiscriminated, + nonDiscriminating, + uncoveredRequirements, +} from './architecture.js' +import { + type MaturityRecord, + type TRL, + TRL_DEFINITION, + assessTrl, + maturationPath, +} from './trl.js' + +export * from './requirements.js' +export * from './architecture.js' +export * from './trl.js' + +export interface MissionConcept { + name: string + needs: Need[] + requirements: Requirement[] + options?: ArchitectureOption[] + /** What has actually been demonstrated. */ + maturity?: MaturityRecord[] + /** The level being claimed today. */ + claimedTrl?: TRL + /** The level the programme is aiming for. */ + targetTrl?: TRL +} + +function requirementFindings(c: MissionConcept): Finding[] { + const issues = requirementIssues(c.requirements ?? [], c.needs ?? []) + const byId = new Map((c.requirements ?? []).map(r => [r.id, r])) + + const out: Finding[] = issues.map(i => { + const r = byId.get(i.requirementId) + return { + id: `req/${i.requirementId}/${i.defect}`, + summary: `${i.requirementId}: ${i.defect.replace('-', ' ')}`, + determination: i.defect === 'invented' || i.defect === 'dangling-parent' ? 'fail' : 'fail', + severity: i.defect === 'invented' ? 'critical' : 'high', + detail: i.detail, + evidence: r ? [evidence.file(r.where, undefined, r.statement.slice(0, 100))] : [], + } + }) + + for (const n of unaddressedNeeds(c.requirements ?? [], c.needs ?? [])) { + out.push({ + id: `need/${n.id}`, + summary: `${n.id} is stated and no requirement covers it`, + determination: 'fail', + severity: 'critical', + detail: `"${n.statement}" (${n.source}). The needs that get forgotten are the ones nobody enjoys thinking about, and a downward-only derivation check never sees them.`, + evidence: [evidence.file(n.where, undefined, n.statement.slice(0, 100), `source: ${n.source}`)], + }) + } + + const clean = (c.requirements ?? []).filter( + r => !issues.some(i => i.requirementId === r.id), + ) + for (const r of clean) { + out.push({ + id: `req/${r.id}`, + summary: `${r.id} traces to ${r.derivedFrom.join(', ')} and is verifiable by ${r.verification!.method}`, + determination: 'pass', + severity: 'info', + detail: `Criterion: ${r.verification!.criterion}`, + evidence: [evidence.file(r.where, undefined, r.statement.slice(0, 100))], + }) + } + + return out +} + +function architectureFindings(c: MissionConcept): Finding[] { + const options = c.options ?? [] + const out: Finding[] = [] + + if (options.length === 0) return out + + if (options.length === 1) { + out.push({ + id: 'arch/single-option', + summary: `Only one architecture option was carried: ${options[0]!.name}`, + determination: 'not-assessed', + severity: 'high', + detail: 'A single architecture with a rationale is a decision already made. There is nothing to trade, so no trade study was performed — whatever the document is called.', + evidence: [evidence.file(options[0]!.where, undefined, options[0]!.name)], + }) + return out + } + + if (isUndiscriminated(options)) { + out.push({ + id: 'arch/undiscriminated', + summary: `No criterion tells the ${options.length} options apart`, + determination: 'fail', + severity: 'critical', + detail: 'Every option scores identically on every stated criterion, so the choice between them rests on something not written down. The table is full and the analysis is absent — which is why this survives review.', + evidence: options.map(o => evidence.file(o.where, undefined, o.name)), + }) + } else { + const flat = nonDiscriminating(options) + if (flat.length > 0) { + out.push({ + id: 'arch/flat-criteria', + summary: `${flat.length} criterion/criteria discriminate nothing: ${flat.join(', ')}`, + determination: 'not-applicable', + severity: 'info', + detail: 'These columns are identical across every option. They are not wrong, they are just not doing any work in the decision.', + evidence: options.map(o => evidence.file(o.where, undefined, o.name)), + }) + } + } + + for (const d of dominatedOptions(options)) { + out.push({ + id: `arch/dominated/${d.dominated.id}`, + summary: `${d.dominated.name} is dominated by ${d.by.name} on requirement coverage`, + determination: 'not-applicable', + severity: 'info', + detail: 'It satisfies a strict subset and adds nothing of its own. It may still win on cost or schedule — but carrying it unexamined makes the study look broader than it is.', + evidence: [evidence.file(d.dominated.where, undefined, d.dominated.name)], + }) + } + + const uncovered = uncoveredRequirements(options, (c.requirements ?? []).map(r => r.id)) + if (uncovered.length > 0) { + out.push({ + id: 'arch/uncovered', + summary: `${uncovered.length} requirement(s) satisfied by no option: ${uncovered.join(', ')}`, + determination: 'fail', + severity: 'critical', + detail: 'No architecture on the table meets these. Either an option is missing or the requirement is.', + evidence: options.map(o => evidence.file(o.where, undefined, o.name)), + }) + } + + return out +} + +function trlFindings(c: MissionConcept): Finding[] { + if (c.claimedTrl === undefined) return [] + const records = c.maturity ?? [] + const a = assessTrl(c.claimedTrl, records) + + if (a.supported === null) { + return [{ + id: 'trl', + summary: `TRL ${c.claimedTrl} is claimed with no demonstration recorded`, + determination: 'not-assessed', + severity: 'critical', + detail: 'No evidence at all does not establish TRL 1 either — TRL 1 is a claim that basic principles were observed and reported, and that is itself a demonstration.', + evidence: [], + }] + } + + const cite = a.strongest + ? [evidence.file(a.strongest.where, undefined, a.strongest.description, `evidence: ${a.strongest.evidence}`)] + : [] + + if (a.capped) { + return [{ + id: 'trl', + summary: `TRL ${c.claimedTrl} claimed, evidence supports TRL ${a.supported}`, + determination: 'fail', + severity: 'critical', + detail: `The strongest demonstration is "${a.strongest!.description}", which supports ${a.supported}: ${TRL_DEFINITION[a.supported]}. TRL ${c.claimedTrl} requires ${TRL_DEFINITION[c.claimedTrl]}. The level is not a judgement call — each one names the environment a thing was demonstrated in, and an environment is a fact.`, + evidence: cite, + }] + } + + return [{ + id: 'trl', + summary: `TRL ${c.claimedTrl} claimed and supported`, + determination: 'pass', + severity: 'info', + detail: TRL_DEFINITION[c.claimedTrl], + evidence: cite, + }] +} + +/** Assess a mission concept's requirements, options and maturity claim. */ +export function synthesize(c: MissionConcept): CapabilityReport { + const needs = c.needs ?? [] + const reqs = c.requirements ?? [] + const options = c.options ?? [] + + if (needs.length === 0 && reqs.length === 0) { + return { + capability: 'MissionSynth', + scope: `${c.name} — nothing supplied`, + examined: 0, + findings: [], + notes: ['No needs and no requirements were supplied. A concept with neither has not been synthesised, only named.'], + } + } + + const findings = [ + ...trlFindings(c), + ...requirementFindings(c), + ...architectureFindings(c), + ] + + const a = c.claimedTrl !== undefined ? assessTrl(c.claimedTrl, c.maturity ?? []) : null + const path = c.targetTrl !== undefined + ? maturationPath(a?.effective ?? null, c.targetTrl) + : [] + + return { + capability: 'MissionSynth', + scope: `${c.name} — ${needs.length} need(s), ${reqs.length} requirement(s), ${options.length} option(s)`, + examined: needs.length + reqs.length + options.length, + findings, + notes: [ + a + ? `TRL: claimed ${a.claimed}, supported ${a.supported ?? 'nothing'}, effective ${a.effective ?? 'unestablished'}` + : 'No TRL claimed.', + ...(path.length > 0 + ? [`maturation to TRL ${c.targetTrl}: ${path.length} step(s) — ${path.map(s => `${s.to}: ${s.produces}`).join(' → ')}`] + : []), + ], + } +} + +/** Render a synthesis for a terminal, a review, or a concept package. */ +export function formatSynthesis(c: MissionConcept, opts: FormatOptions = {}): string { + return renderReport(synthesize(c), opts) +} diff --git a/src/requirements.ts b/src/requirements.ts new file mode 100644 index 0000000..1ee971c --- /dev/null +++ b/src/requirements.ts @@ -0,0 +1,140 @@ +/** + * Requirements derived from needs, checked in BOTH directions. + * + * The two defects a generated requirement set reliably has are opposites, + * and a one-directional check finds only one of them: + * + * - **An invention.** A requirement that traces to no stated need. It reads + * perfectly well, it is plausible, and nobody asked for it. This is the + * characteristic failure of a synthesised set, because generating + * plausible requirements is easy and generating only warranted ones is + * not. + * + * - **An unaddressed need.** A stated need that no requirement covers. This + * is the characteristic failure of a hand-written set, because the needs + * that get forgotten are the ones nobody enjoys thinking about. + * + * Checking derivation only downward finds inventions and misses gaps; + * checking coverage only upward finds gaps and misses inventions. Both run. + * + * The verifiability check is separate and older than any of this: a + * requirement that names no verification method and no measurable criterion + * is a wish. "The system shall be user-friendly" cannot be shown false, so + * it cannot be shown true. + */ + +export interface Need { + id: string + statement: string + /** Who or what it comes from — a stakeholder, a standard, a constraint. */ + source: string + where: string +} + +export type VerificationMethod = 'test' | 'analysis' | 'inspection' | 'demonstration' + +export interface Verification { + method: VerificationMethod + /** + * The acceptance criterion. Must be decidable — a number, a threshold, a + * pass condition. "Acceptable performance" is not a criterion. + */ + criterion: string +} + +export interface Requirement { + id: string + statement: string + /** Need ids this was derived from. Empty means it was invented. */ + derivedFrom: string[] + verification?: Verification + where: string +} + +export type RequirementDefect = + | 'invented' + | 'dangling-parent' + | 'unverifiable' + | 'immeasurable' + +export interface RequirementIssue { + requirementId: string + defect: RequirementDefect + detail: string +} + +/** + * Words that describe a feeling rather than a threshold. + * + * Deliberately short and deliberately not exhaustive. This is a smell test + * that catches the common cases, not a grammar — a criterion can be + * unmeasurable without using any of these words, which is why the absence of + * a hit is not evidence of a good criterion. + */ +const VAGUE = [ + 'user-friendly', 'user friendly', 'easy to use', 'intuitive', 'robust', + 'reliable', 'fast', 'efficient', 'as needed', 'as appropriate', + 'sufficient', 'adequate', 'acceptable', 'reasonable', 'minimal', + 'state of the art', 'best effort', 'if possible', 'etc', +] + +/** True when the text names something that could be measured or observed. */ +export function isMeasurable(text: string): boolean { + const lower = text.toLowerCase() + if (VAGUE.some(v => lower.includes(v))) return false + // A decidable criterion almost always carries a number, a comparison, or + // an explicit pass condition. + return /\d/.test(lower) + || /\b(shall|must)\s+(not\s+)?(equal|exceed|match|contain|return|reject|be)\b/.test(lower) + || /\b(pass|fail|present|absent|within|between|less than|greater than|at least|at most)\b/.test(lower) +} + +/** Requirements that are unwarranted, unverifiable, or both. */ +export function requirementIssues( + requirements: Requirement[], + needs: Need[], +): RequirementIssue[] { + const needIds = new Set((needs ?? []).map(n => n.id)) + const out: RequirementIssue[] = [] + + for (const r of requirements ?? []) { + if (r.derivedFrom.length === 0) { + out.push({ + requirementId: r.id, + defect: 'invented', + detail: 'Traces to no stated need. It may be a good idea; it is not a derived requirement, and nothing in the concept asked for it.', + }) + } else { + const dangling = r.derivedFrom.filter(id => !needIds.has(id)) + if (dangling.length > 0) { + out.push({ + requirementId: r.id, + defect: 'dangling-parent', + detail: `Derived from ${dangling.join(', ')}, which ${dangling.length === 1 ? 'is not a' : 'are not'} stated need${dangling.length === 1 ? '' : 's'}. A parent that does not exist is the same as no parent, and it looks like traceability.`, + }) + } + } + + if (!r.verification) { + out.push({ + requirementId: r.id, + defect: 'unverifiable', + detail: 'No verification method. A requirement nobody can show to be met is a wish — and it will pass every review, because there is nothing to fail.', + }) + } else if (!isMeasurable(r.verification.criterion)) { + out.push({ + requirementId: r.id, + defect: 'immeasurable', + detail: `Acceptance criterion "${r.verification.criterion}" cannot be decided. Name a threshold, a count, or an explicit pass condition.`, + }) + } + } + + return out +} + +/** Needs no requirement covers. */ +export function unaddressedNeeds(requirements: Requirement[], needs: Need[]): Need[] { + const covered = new Set((requirements ?? []).flatMap(r => r.derivedFrom)) + return (needs ?? []).filter(n => !covered.has(n.id)) +} diff --git a/src/trl.ts b/src/trl.ts new file mode 100644 index 0000000..ef96bed --- /dev/null +++ b/src/trl.ts @@ -0,0 +1,181 @@ +/** + * TRL, capped by evidence rather than asserted. + * + * Technology Readiness Level is the most self-assessed number in the + * business. The gap between "we are at TRL 6" and "here is what would + * demonstrate TRL 6" is where programmes quietly go wrong, and it goes wrong + * in one direction only — nobody ever claims a level below the one they can + * show. + * + * The definitions are not vague, and that is what makes this checkable. Each + * level names the ENVIRONMENT in which something was demonstrated, and an + * environment is a fact: + * + * TRL 4 component validated in a LABORATORY + * TRL 5 component validated in a RELEVANT environment + * TRL 6 system model demonstrated in a RELEVANT environment + * TRL 7 system prototype in an OPERATIONAL environment + * + * So a claim of TRL 6 backed by a passing unit test is not a judgement call + * that reasonable people might differ on. A unit test is a laboratory + * activity on a component, and it supports TRL 4. + * + * Same discipline as `@extant2000/sustain-score`: cap by the weakest evidence, + * never average, and report the cap rather than silently applying it. + */ + +export type TRL = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 + +/** What was actually done, as opposed to what level was claimed. */ +export type MaturityEvidence = + /** Principles observed and reported. Paper. */ + | 'concept-paper' + /** Technology concept formulated — analysis, no experiment. */ + | 'analysis' + /** Analytical and experimental proof of concept. A bench or a unit test. */ + | 'lab-bench' + /** Component validated in a laboratory. */ + | 'component-lab' + /** Component validated in a RELEVANT environment. */ + | 'component-relevant-env' + /** System or subsystem model demonstrated in a RELEVANT environment. */ + | 'system-relevant-env' + /** System prototype demonstrated in an OPERATIONAL environment. */ + | 'prototype-operational-env' + /** Actual system completed and qualified through test and demonstration. */ + | 'qualified' + /** Actual system proven through successful mission operations. */ + | 'operational-mission' + +/** + * The highest TRL each kind of evidence can support. + * + * Read this as a ceiling, not a score. Evidence at `component-lab` does not + * mean you ARE at TRL 4; it means you cannot be above it on this evidence. + */ +export const EVIDENCE_CEILING: Record = { + 'concept-paper': 1, + analysis: 2, + 'lab-bench': 3, + 'component-lab': 4, + 'component-relevant-env': 5, + 'system-relevant-env': 6, + 'prototype-operational-env': 7, + qualified: 8, + 'operational-mission': 9, +} + +/** Plain-language description of what each level actually requires. */ +export const TRL_DEFINITION: Record = { + 1: 'basic principles observed and reported', + 2: 'technology concept formulated', + 3: 'analytical and experimental proof of concept', + 4: 'component validated in a laboratory', + 5: 'component validated in a relevant environment', + 6: 'system or subsystem model demonstrated in a relevant environment', + 7: 'system prototype demonstrated in an operational environment', + 8: 'actual system completed and qualified through test and demonstration', + 9: 'actual system proven through successful mission operations', +} + +export interface MaturityRecord { + /** What kind of demonstration this was. */ + evidence: MaturityEvidence + /** One line on what was actually done. */ + description: string + /** Where it is recorded — a report, a test id, a campaign. */ + where: string +} + +export interface TrlAssessment { + claimed: TRL + /** The highest level the supplied evidence supports. */ + supported: TRL | null + /** `supported` when it is lower than `claimed`, otherwise `claimed`. */ + effective: TRL | null + /** True when the claim ran ahead of the evidence. */ + capped: boolean + /** The strongest record, which is the one that set the ceiling. */ + strongest?: MaturityRecord +} + +/** + * The highest level the evidence supports. + * + * The BEST evidence sets the ceiling here, not the worst — TRL is a + * high-water mark of demonstration, and having also written a paper does not + * drag down a system demonstrated in a relevant environment. The + * anti-inflation move is elsewhere: the claim is capped to this, and no + * amount of claiming raises it. + * + * Returns `null` for no evidence. Not TRL 1 — TRL 1 is a claim that + * principles were observed and reported, and no evidence at all does not + * establish even that. + */ +export function supportedTrl(records: MaturityRecord[]): TRL | null { + const list = records ?? [] + if (list.length === 0) return null + return list.reduce((hi, r) => { + const c = EVIDENCE_CEILING[r.evidence] + return c > hi ? c : hi + }, 1 as TRL) +} + +/** Assess a claimed TRL against what was actually demonstrated. */ +export function assessTrl(claimed: TRL, records: MaturityRecord[]): TrlAssessment { + const list = records ?? [] + const supported = supportedTrl(list) + + if (supported === null) { + return { claimed, supported: null, effective: null, capped: false } + } + + const strongest = [...list].sort( + (a, b) => EVIDENCE_CEILING[b.evidence] - EVIDENCE_CEILING[a.evidence], + )[0] + + return { + claimed, + supported, + effective: Math.min(claimed, supported) as TRL, + capped: supported < claimed, + strongest, + } +} + +export interface MaturationStep { + from: TRL + to: TRL + /** What has to be done. */ + activity: string + /** The evidence that activity produces — this is what raises the level. */ + produces: MaturityEvidence +} + +/** + * The steps between where the evidence puts you and where you want to be. + * + * Generated from the level definitions rather than invented, so each step + * names the demonstration that actually moves the number. A roadmap whose + * steps do not produce evidence is a schedule, not a maturation plan. + */ +export function maturationPath(from: TRL | null, to: TRL): MaturationStep[] { + const start = from ?? 0 + if (to <= start) return [] + + const byLevel = new Map() + for (const [ev, lvl] of Object.entries(EVIDENCE_CEILING) as [MaturityEvidence, TRL][]) { + byLevel.set(lvl, ev) + } + + const steps: MaturationStep[] = [] + for (let lvl = (start + 1) as TRL; lvl <= to; lvl = (lvl + 1) as TRL) { + steps.push({ + from: Math.max(start, lvl - 1) as TRL, + to: lvl, + activity: `Demonstrate: ${TRL_DEFINITION[lvl]}`, + produces: byLevel.get(lvl)!, + }) + } + return steps +} diff --git a/test/mission-synth.test.ts b/test/mission-synth.test.ts new file mode 100644 index 0000000..6c3abba --- /dev/null +++ b/test/mission-synth.test.ts @@ -0,0 +1,267 @@ +import { describe, expect, it } from 'vitest' +import { overall, validateReport } from '@extant2000/evidence-record' +import { + assessTrl, + dominatedOptions, + formatSynthesis, + isMeasurable, + isUndiscriminated, + maturationPath, + nonDiscriminating, + requirementIssues, + supportedTrl, + synthesize, + unaddressedNeeds, + type MissionConcept, + type Need, + type Requirement, +} from '../src/index.js' + +const need = (p: Partial = {}): Need => ({ + id: 'N-1', + statement: 'the spacecraft must deorbit within 25 years of end of mission', + source: 'ODMSP 2019', + where: 'concept/needs.md', + ...p, +}) + +const req = (p: Partial = {}): Requirement => ({ + id: 'SYS-001', + statement: 'The system shall deorbit within 25 years', + derivedFrom: ['N-1'], + verification: { method: 'analysis', criterion: 'orbital decay analysis shows re-entry at less than 25 years' }, + where: 'concept/requirements.md', + ...p, +}) + +describe('derivation is checked in both directions', () => { + it('flags an invented requirement — the failure of a synthesised set', () => { + // Generating plausible requirements is easy; generating only warranted + // ones is not. + const issues = requirementIssues([req({ derivedFrom: [] })], [need()]) + expect(issues.some(i => i.defect === 'invented')).toBe(true) + expect(issues.find(i => i.defect === 'invented')!.detail) + .toContain('nothing in the concept asked for it') + }) + + it('flags an unaddressed need — the failure of a hand-written set', () => { + // A downward-only check never sees this. + const orphanNeed = need({ id: 'N-2', statement: 'must survive launch loads' }) + expect(unaddressedNeeds([req()], [need(), orphanNeed]).map(n => n.id)).toEqual(['N-2']) + }) + + it('rates an invented requirement critical and reports both directions together', () => { + const c: MissionConcept = { + name: 'test', + needs: [need(), need({ id: 'N-2', statement: 'survive launch' })], + requirements: [req(), req({ id: 'SYS-009', derivedFrom: [] })], + } + const r = synthesize(c) + expect(r.findings.some(f => f.id === 'req/SYS-009/invented')).toBe(true) + expect(r.findings.some(f => f.id === 'need/N-2')).toBe(true) + expect(overall(r)).toBe('fail') + }) + + it('catches a parent that does not exist — traceability that looks real', () => { + const issues = requirementIssues([req({ derivedFrom: ['N-99'] })], [need()]) + const d = issues.find(i => i.defect === 'dangling-parent')! + expect(d.detail).toContain('it looks like traceability') + }) +}) + +describe('a requirement nobody can fail is not a requirement', () => { + it('flags a missing verification method', () => { + const issues = requirementIssues([req({ verification: undefined })], [need()]) + const u = issues.find(i => i.defect === 'unverifiable')! + expect(u.detail).toContain('there is nothing to fail') + }) + + it('rejects a criterion that describes a feeling', () => { + expect(isMeasurable('the interface shall be user-friendly')).toBe(false) + expect(isMeasurable('performance shall be acceptable')).toBe(false) + expect(isMeasurable('response time shall be less than 200 ms')).toBe(true) + expect(isMeasurable('the request shall be rejected')).toBe(true) + }) + + it('flags an immeasurable criterion', () => { + const issues = requirementIssues( + [req({ verification: { method: 'inspection', criterion: 'the design is robust' } })], + [need()], + ) + expect(issues.some(i => i.defect === 'immeasurable')).toBe(true) + }) + + it('passes a properly derived, verifiable requirement', () => { + expect(requirementIssues([req()], [need()])).toEqual([]) + expect(overall(synthesize({ name: 't', needs: [need()], requirements: [req()] }))).toBe('pass') + }) +}) + +describe('TRL is capped by evidence, never asserted', () => { + it('REPRODUCES THE CLASSIC OVERCLAIM: TRL 6 backed by a unit test', () => { + // A unit test is a laboratory activity on a component. It supports 4. + const a = assessTrl(6, [ + { evidence: 'lab-bench', description: 'unit test suite passes', where: 'ci/unit-tests' }, + ]) + expect(a.supported).toBe(3) + expect(a.effective).toBe(3) + expect(a.capped).toBe(true) + }) + + it('explains the cap by naming the environment each level requires', () => { + const c: MissionConcept = { + name: 'deorbit module', needs: [need()], requirements: [req()], + claimedTrl: 6, + maturity: [{ evidence: 'component-lab', description: 'thruster fired on the bench', where: 'test/TR-12' }], + } + const f = synthesize(c).findings.find(x => x.id === 'trl')! + expect(f.determination).toBe('fail') + expect(f.detail).toContain('an environment is a fact') + expect(f.detail).toContain('system or subsystem model demonstrated in a relevant environment') + }) + + it('takes the BEST evidence as the ceiling, not the worst', () => { + // Having also written a paper does not drag down a system demonstration. + expect(supportedTrl([ + { evidence: 'concept-paper', description: 'white paper', where: 'a' }, + { evidence: 'system-relevant-env', description: 'thermal vac campaign', where: 'b' }, + ])).toBe(6) + }) + + it('returns null for no evidence — not TRL 1', () => { + // TRL 1 is itself a claim that principles were observed and reported. + expect(supportedTrl([])).toBeNull() + const f = synthesize({ + name: 't', needs: [need()], requirements: [req()], claimedTrl: 4, + }).findings.find(x => x.id === 'trl')! + expect(f.determination).toBe('not-assessed') + expect(f.detail).toContain('does not establish TRL 1 either') + }) + + it('accepts a claim the evidence supports', () => { + const a = assessTrl(5, [ + { evidence: 'component-relevant-env', description: 'vibe test at flight levels', where: 'TR-30' }, + ]) + expect(a.capped).toBe(false) + expect(a.effective).toBe(5) + }) + + it('does not raise a modest claim to the evidence ceiling', () => { + const a = assessTrl(3, [{ evidence: 'qualified', description: 'full qual', where: 'x' }]) + expect(a.effective).toBe(3) + expect(a.capped).toBe(false) + }) +}) + +describe('the maturation path names demonstrations, not dates', () => { + it('produces one step per level, each with the evidence that raises it', () => { + const path = maturationPath(4, 6) + expect(path.map(s => s.to)).toEqual([5, 6]) + expect(path[0]!.produces).toBe('component-relevant-env') + expect(path[1]!.produces).toBe('system-relevant-env') + expect(path[1]!.activity).toContain('relevant environment') + }) + + it('starts from zero when nothing has been demonstrated', () => { + expect(maturationPath(null, 3).map(s => s.to)).toEqual([1, 2, 3]) + }) + + it('is empty when the target is already met', () => { + expect(maturationPath(7, 6)).toEqual([]) + }) + + it('is reported from the EFFECTIVE level, not the claimed one', () => { + const text = formatSynthesis({ + name: 't', needs: [need()], requirements: [req()], + claimedTrl: 6, targetTrl: 7, + maturity: [{ evidence: 'lab-bench', description: 'unit tests', where: 'ci' }], + }) + // effective is 3, so the path is 4..7 — four steps, not one. + expect(text).toContain('4 step(s)') + }) +}) + +describe('a trade study with nothing to trade', () => { + const opt = (id: string, criteria: Record, satisfies: string[] = ['SYS-001']) => + ({ id, name: `option ${id}`, satisfies, criteria, where: `trade/${id}.md` }) + + it('flags a single option as no trade study at all', () => { + const f = synthesize({ + name: 't', needs: [need()], requirements: [req()], + options: [opt('A', { cost: 1 })], + }).findings.find(x => x.id === 'arch/single-option')! + expect(f.determination).toBe('not-assessed') + expect(f.detail).toContain('a decision already made') + }) + + it('CATCHES THE THEATRE: options identical on every criterion', () => { + // The table is full and the analysis is absent. + const options = [opt('A', { cost: 3, mass: 10 }), opt('B', { cost: 3, mass: 10 })] + expect(isUndiscriminated(options)).toBe(true) + const f = synthesize({ name: 't', needs: [need()], requirements: [req()], options }) + .findings.find(x => x.id === 'arch/undiscriminated')! + expect(f.determination).toBe('fail') + expect(f.detail).toContain('which is why this survives review') + }) + + it('names the columns doing no work when only some are flat', () => { + const options = [opt('A', { cost: 3, mass: 10 }), opt('B', { cost: 5, mass: 10 })] + expect(nonDiscriminating(options)).toEqual(['mass']) + expect(isUndiscriminated(options)).toBe(false) + }) + + it('flags an option dominated on coverage', () => { + const options = [ + opt('A', { cost: 1 }, ['SYS-001']), + opt('B', { cost: 2 }, ['SYS-001', 'SYS-002']), + ] + const d = dominatedOptions(options) + expect(d).toHaveLength(1) + expect(d[0]!.dominated.id).toBe('A') + }) + + it('does not call an option dominated when it covers something unique', () => { + const options = [ + opt('A', { cost: 1 }, ['SYS-003']), + opt('B', { cost: 2 }, ['SYS-001', 'SYS-002']), + ] + expect(dominatedOptions(options)).toEqual([]) + }) + + it('fails when no option satisfies a requirement', () => { + const f = synthesize({ + name: 't', needs: [need()], requirements: [req(), req({ id: 'SYS-002' })], + options: [opt('A', { cost: 1 }, ['SYS-001']), opt('B', { cost: 2 }, ['SYS-001'])], + }).findings.find(x => x.id === 'arch/uncovered')! + expect(f.determination).toBe('fail') + expect(f.summary).toContain('SYS-002') + }) +}) + +describe('conformance with the evidence-record standard', () => { + it('an empty concept is not-assessed, and says what empty means', () => { + const r = synthesize({ name: 'unnamed', needs: [], requirements: [] }) + expect(overall(r)).toBe('not-assessed') + expect(formatSynthesis({ name: 'unnamed', needs: [], requirements: [] })) + .toContain('has not been synthesised, only named') + }) + + it('every conclusion carries a citation', () => { + expect(validateReport(synthesize({ + name: 't', + needs: [need(), need({ id: 'N-2' })], + requirements: [req(), req({ id: 'SYS-009', derivedFrom: [] })], + claimedTrl: 6, + maturity: [{ evidence: 'lab-bench', description: 'unit tests', where: 'ci' }], + options: [ + { id: 'A', name: 'A', satisfies: ['SYS-001'], criteria: { cost: 1 }, where: 'a.md' }, + { id: 'B', name: 'B', satisfies: ['SYS-001'], criteria: { cost: 2 }, where: 'b.md' }, + ], + }))).toEqual([]) + }) + + it('hands off to TraceProof on the requirement id', () => { + const r = synthesize({ name: 't', needs: [need()], requirements: [req()] }) + expect(r.findings.some(f => f.id === 'req/SYS-001')).toBe(true) + }) +}) diff --git a/tsconfig.cjs.json b/tsconfig.cjs.json new file mode 100644 index 0000000..0d99093 --- /dev/null +++ b/tsconfig.cjs.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "CommonJS", + "moduleResolution": "Node", + "outDir": "dist-cjs", + "declaration": false + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..9f56c30 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "target": "ES2022", "module": "ES2022", "moduleResolution": "bundler", + "declaration": true, "outDir": "dist", "rootDir": "src", + "strict": true, "skipLibCheck": true + }, + "include": ["src/**/*.ts"] +}