@disclosureos/scoring 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/LICENSE +21 -0
- package/README.md +89 -0
- package/dist/compellingness/index.d.ts +99 -0
- package/dist/compellingness/index.js +136 -0
- package/dist/compellingness/index.js.map +1 -0
- package/dist/completeness/index.d.ts +41 -0
- package/dist/completeness/index.js +109 -0
- package/dist/completeness/index.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +252 -0
- package/dist/index.js.map +1 -0
- package/package.json +81 -0
- package/schema/scoring.schema.json +194 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@disclosureos/scoring` are documented here. The format is
|
|
4
|
+
based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project
|
|
5
|
+
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## 1.0.0
|
|
8
|
+
|
|
9
|
+
Initial public release — the Scoring layer.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- `getCompleteness` (how well-documented a record is) and `score` /
|
|
13
|
+
`rankByCompellingness` (how anomalous a case is).
|
|
14
|
+
- Scoring over arrays of competing claims: results include a `range` (spread across
|
|
15
|
+
claims) and a `contested` flag when evaluators disagree.
|
|
16
|
+
- `DEFAULT_WEIGHTS` and configurable evaluator weighting.
|
|
17
|
+
- Committed JSON Schema artifact (`schema/scoring.schema.json`, draft 2020-12).
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Disclosure Foundation
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# @disclosureos/scoring
|
|
2
|
+
|
|
3
|
+
[](https://opensource.org/licenses/MIT)
|
|
4
|
+
|
|
5
|
+
Reference scoring for the [DisclosureOS](https://github.com/disclosurefoundation)
|
|
6
|
+
ecosystem — the **Scoring** layer. It turns an enriched `Observation` into two
|
|
7
|
+
orthogonal measures:
|
|
8
|
+
|
|
9
|
+
- **Completeness** — *is it well-documented?* What fraction of the record's fields are
|
|
10
|
+
present.
|
|
11
|
+
- **Compellingness** — *is it anomalous / non-mundane?* Derived from the observable
|
|
12
|
+
assessments and origin classification.
|
|
13
|
+
|
|
14
|
+
## The DisclosureOS model
|
|
15
|
+
|
|
16
|
+
DisclosureOS is a **five-part standard**. Each part answers one question about an
|
|
17
|
+
observation; `@disclosureos/schema` binds the package-owned pieces into one portable
|
|
18
|
+
contract (a single TS type + JSON Schema + non-stripping parse).
|
|
19
|
+
|
|
20
|
+
| Part | Package surface | Question it answers |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| **Records** | `@disclosureos/records` | *What was observed?* |
|
|
23
|
+
| **Observables** | `@disclosureos/observables` | *What anomalous characteristics did it show?* |
|
|
24
|
+
| **Origins** | `@disclosureos/origins` | *What might explain it?* |
|
|
25
|
+
| **Claims** | `@disclosureos/records/shared` | *Who assessed it, why, and on what evidence?* |
|
|
26
|
+
| **Scoring** | `@disclosureos/scoring` | *How complete / compelling is the case?* |
|
|
27
|
+
|
|
28
|
+
Supporting packages: **`@disclosureos/schema`** (the portable contract that composes the
|
|
29
|
+
package-owned pieces), **`@disclosureos/cli`** (tooling), and **`@disclosureos/examples`**
|
|
30
|
+
(the runnable golden path). *(You are here: **Scoring**.)*
|
|
31
|
+
|
|
32
|
+
## Consumes the foundation (does not augment it)
|
|
33
|
+
|
|
34
|
+
Unlike `observables` and `origins`, scoring **only consumes** the foundation — it
|
|
35
|
+
reads `observableAssessments` and `origin` but adds no slot to `Observation`. It
|
|
36
|
+
depends on `@disclosureos/records`, `@disclosureos/observables`, and
|
|
37
|
+
`@disclosureos/origins`, so importing it gives you those slots typed automatically.
|
|
38
|
+
|
|
39
|
+
> **See it end to end.** [`examples/golden-path.ts`](../../examples/golden-path.ts)
|
|
40
|
+
> takes one observation through every layer — records → observables → origins → scoring,
|
|
41
|
+
> validated as a whole by `@disclosureos/schema` — in a single type-checked file. Run it
|
|
42
|
+
> with `pnpm --filter @disclosureos/examples golden-path`.
|
|
43
|
+
>
|
|
44
|
+
> **Migrating an existing dataset?** See [`examples/migration-path.ts`](../../examples/migration-path.ts)
|
|
45
|
+
> and the [onboarding workspace](https://os.disclosure.org/onboard).
|
|
46
|
+
|
|
47
|
+
## Install
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pnpm add @disclosureos/scoring @disclosureos/records @disclosureos/observables @disclosureos/origins zod
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Quick start
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
import { score, getCompleteness, rankByCompellingness } from '@disclosureos/scoring';
|
|
57
|
+
|
|
58
|
+
const result = score(observation);
|
|
59
|
+
// { score, range: { low, high }, contested, ... }
|
|
60
|
+
|
|
61
|
+
const completeness = getCompleteness(observation);
|
|
62
|
+
// { percentage, requiredPercentage, missing, ... }
|
|
63
|
+
|
|
64
|
+
const ranked = rankByCompellingness([obsA, obsB, obsC]); // most-compelling first
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Because evaluative slots hold **arrays of competing claims**, `score()` reports a
|
|
68
|
+
`range` (the spread across claims) and a `contested` flag when evaluators disagree —
|
|
69
|
+
not just a single point estimate.
|
|
70
|
+
|
|
71
|
+
## Subpath exports
|
|
72
|
+
|
|
73
|
+
| Subpath | Contents |
|
|
74
|
+
|---|---|
|
|
75
|
+
| `@disclosureos/scoring` | `score`, `rankByCompellingness`, `getCompleteness`, `DEFAULT_WEIGHTS` |
|
|
76
|
+
| `@disclosureos/scoring/completeness` | Completeness scoring + `deriveFieldPaths` |
|
|
77
|
+
| `@disclosureos/scoring/compellingness` | Compellingness scoring + weights |
|
|
78
|
+
| `@disclosureos/scoring/schema` | The committed JSON Schema (`scoring.schema.json`) |
|
|
79
|
+
|
|
80
|
+
## Standard Schema
|
|
81
|
+
|
|
82
|
+
The Zod schemas here (`ScoreResultSchema`, `CompletenessResultSchema`, …) implement
|
|
83
|
+
[Standard Schema v1](https://standardschema.dev) via their `~standard` property — a
|
|
84
|
+
guarantee of Zod 4. Any Standard-Schema-compatible validator or framework can consume
|
|
85
|
+
them directly, with no DisclosureOS-specific adapter.
|
|
86
|
+
|
|
87
|
+
## License
|
|
88
|
+
|
|
89
|
+
MIT © [Disclosure Foundation](https://disclosure.org)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { Observation } from '@disclosureos/records';
|
|
3
|
+
import { AssessmentLevel } from '@disclosureos/observables';
|
|
4
|
+
|
|
5
|
+
/** Methodology version stamped on every {@link ScoreResult}. Bump on weight/formula changes. */
|
|
6
|
+
declare const SCORING_VERSION: "2.0.0";
|
|
7
|
+
/**
|
|
8
|
+
* Ordinal anomaly strength per evidentiary tier. A `confirmed` signal counts
|
|
9
|
+
* fully; `not_indicated` contributes nothing. Multiplied by the assessment's
|
|
10
|
+
* confidence (default 1 when unstated). Exported so consumers can render the
|
|
11
|
+
* methodology; override semantics may come in a future minor release.
|
|
12
|
+
*/
|
|
13
|
+
declare const ASSESSMENT_LEVEL_WEIGHTS: Record<AssessmentLevel, number>;
|
|
14
|
+
/**
|
|
15
|
+
* OCS id prefixes treated as prosaic (well-explained). A confident *prosaic*
|
|
16
|
+
* classification means the case is explained, so it contributes no anomaly
|
|
17
|
+
* signal. Overridable per {@link score} call. Default is the OCS "Prosaic"
|
|
18
|
+
* branch (`1.1.1`).
|
|
19
|
+
*/
|
|
20
|
+
declare const DEFAULT_PROSAIC_OCS_PREFIXES: readonly string[];
|
|
21
|
+
declare const CompellingnessWeightsSchema: z.ZodObject<{
|
|
22
|
+
technology: z.ZodNumber;
|
|
23
|
+
biologics: z.ZodNumber;
|
|
24
|
+
origin: z.ZodNumber;
|
|
25
|
+
}, z.core.$strip>;
|
|
26
|
+
type CompellingnessWeights = z.infer<typeof CompellingnessWeightsSchema>;
|
|
27
|
+
/**
|
|
28
|
+
* Fixed default weights, exposed as a transparent, overridable data object (not a
|
|
29
|
+
* config system). Partners can propose alternate weight sets without an API change.
|
|
30
|
+
*
|
|
31
|
+
* @experimental The *shape* (`CompellingnessWeights`) is stable, but these specific
|
|
32
|
+
* default values are a reference methodology that may be re-tuned in a minor release.
|
|
33
|
+
* Pin your own `weights` via {@link ScoreOptions} if you need stable numbers.
|
|
34
|
+
*/
|
|
35
|
+
declare const DEFAULT_WEIGHTS: CompellingnessWeights;
|
|
36
|
+
declare const ScoreResultSchema: z.ZodObject<{
|
|
37
|
+
score: z.ZodNumber;
|
|
38
|
+
range: z.ZodObject<{
|
|
39
|
+
low: z.ZodNumber;
|
|
40
|
+
high: z.ZodNumber;
|
|
41
|
+
}, z.core.$strip>;
|
|
42
|
+
contested: z.ZodBoolean;
|
|
43
|
+
components: z.ZodObject<{
|
|
44
|
+
technology: z.ZodNumber;
|
|
45
|
+
biologics: z.ZodNumber;
|
|
46
|
+
origin: z.ZodNumber;
|
|
47
|
+
}, z.core.$strip>;
|
|
48
|
+
weights: z.ZodObject<{
|
|
49
|
+
technology: z.ZodNumber;
|
|
50
|
+
biologics: z.ZodNumber;
|
|
51
|
+
origin: z.ZodNumber;
|
|
52
|
+
}, z.core.$strip>;
|
|
53
|
+
scoringVersion: z.ZodString;
|
|
54
|
+
}, z.core.$strip>;
|
|
55
|
+
type ScoreResult = z.infer<typeof ScoreResultSchema>;
|
|
56
|
+
/** The minimal claim shape the trust hook sees (it never needs the full body). */
|
|
57
|
+
type EvaluatorWeightInput = {
|
|
58
|
+
evaluatedBy?: string | undefined;
|
|
59
|
+
evidenceRefs?: string[] | undefined;
|
|
60
|
+
};
|
|
61
|
+
interface ScoreOptions {
|
|
62
|
+
weights?: CompellingnessWeights;
|
|
63
|
+
/** Override the prosaic OCS prefixes (defaults to {@link DEFAULT_PROSAIC_OCS_PREFIXES}). */
|
|
64
|
+
prosaicPrefixes?: readonly string[];
|
|
65
|
+
/**
|
|
66
|
+
* Trust applied to each claim's vote in the consensus point estimate (default
|
|
67
|
+
* 1.0 — uniform). This is where a future trust policy (e.g.
|
|
68
|
+
* weighting verified/official evaluators higher) plugs in here without changing
|
|
69
|
+
* the formula. Range/contestedness are NOT affected — they report the honest
|
|
70
|
+
* spread of what evaluators actually asserted.
|
|
71
|
+
*
|
|
72
|
+
* @experimental The hook signature is stable, but the default trust *policy*
|
|
73
|
+
* (uniform 1.0) is deliberately unopinionated and is a separate, ongoing design
|
|
74
|
+
* task; a future built-in policy may change the default.
|
|
75
|
+
*/
|
|
76
|
+
evaluatorWeight?: (claim: EvaluatorWeightInput) => number;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Reference compellingness scorer. Combines the strongest technology- and
|
|
80
|
+
* biologics-anomaly signals (from `observableAssessments`) with the confidence in
|
|
81
|
+
* a *non-prosaic* origin classification (from `origin`), under transparent,
|
|
82
|
+
* overridable weights. Each slot is a *list* of competing claims, so the scorer
|
|
83
|
+
* reports a consensus `score`, the `range` of readings across claims, and whether
|
|
84
|
+
* the case is `contested` (evaluators disagree on direction). This answers "how
|
|
85
|
+
* strongly does this case bear on the two public questions — anomalous
|
|
86
|
+
* tech/biologics, and a non-mundane origin?" — distinct from completeness.
|
|
87
|
+
*/
|
|
88
|
+
declare function score(observation: Observation, options?: ScoreOptions): ScoreResult;
|
|
89
|
+
interface RankedObservation<T extends Observation = Observation> {
|
|
90
|
+
observation: T;
|
|
91
|
+
result: ScoreResult;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Score a set of observations and return them sorted most-compelling first — the
|
|
95
|
+
* "float the strongest cases to the top" use case.
|
|
96
|
+
*/
|
|
97
|
+
declare function rankByCompellingness<T extends Observation>(observations: readonly T[], options?: ScoreOptions): RankedObservation<T>[];
|
|
98
|
+
|
|
99
|
+
export { ASSESSMENT_LEVEL_WEIGHTS, type CompellingnessWeights, CompellingnessWeightsSchema, DEFAULT_PROSAIC_OCS_PREFIXES, DEFAULT_WEIGHTS, type EvaluatorWeightInput, type RankedObservation, SCORING_VERSION, type ScoreOptions, type ScoreResult, ScoreResultSchema, rankByCompellingness, score };
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import '@disclosureos/observables';
|
|
3
|
+
import '@disclosureos/origins';
|
|
4
|
+
|
|
5
|
+
// src/compellingness/index.ts
|
|
6
|
+
var SCORING_VERSION = "2.0.0";
|
|
7
|
+
var ASSESSMENT_LEVEL_WEIGHTS = {
|
|
8
|
+
not_indicated: 0,
|
|
9
|
+
reported: 0.25,
|
|
10
|
+
documented: 0.5,
|
|
11
|
+
measured: 0.75,
|
|
12
|
+
confirmed: 1
|
|
13
|
+
};
|
|
14
|
+
var DEFAULT_PROSAIC_OCS_PREFIXES = ["1.1.1"];
|
|
15
|
+
var CompellingnessWeightsSchema = z.object({
|
|
16
|
+
technology: z.number().min(0).describe("Weight of the technology-anomaly signal."),
|
|
17
|
+
biologics: z.number().min(0).describe("Weight of the biologics-anomaly signal."),
|
|
18
|
+
origin: z.number().min(0).describe("Weight of the (non-prosaic) origin-confidence signal.")
|
|
19
|
+
}).meta({ id: "CompellingnessWeights" }).describe("Relative weights for the compellingness components. Need not sum to 1 \u2014 normalized internally.");
|
|
20
|
+
var DEFAULT_WEIGHTS = {
|
|
21
|
+
technology: 0.45,
|
|
22
|
+
biologics: 0.35,
|
|
23
|
+
origin: 0.2
|
|
24
|
+
};
|
|
25
|
+
var ScoreResultSchema = z.object({
|
|
26
|
+
score: z.number().min(0).max(1).describe("Overall compellingness (consensus point estimate), 0\u20131."),
|
|
27
|
+
range: z.object({
|
|
28
|
+
low: z.number().min(0).max(1).describe("Most-skeptical reading of the strongest-anomaly signal across competing claims."),
|
|
29
|
+
high: z.number().min(0).max(1).describe("Most-anomalous reading of the strongest-anomaly signal across competing claims.")
|
|
30
|
+
}).describe(
|
|
31
|
+
"Spread of the score across competing claims \u2014 wide range signals disagreement. Bounds are computed per-signal under the same strongest-anomaly (max) reduction as the point estimate, not a global min/max over every claim."
|
|
32
|
+
),
|
|
33
|
+
contested: z.boolean().describe("True when claims disagree on *direction* (some assert anomaly, some assert none)."),
|
|
34
|
+
components: z.object({
|
|
35
|
+
technology: z.number().min(0).max(1),
|
|
36
|
+
biologics: z.number().min(0).max(1),
|
|
37
|
+
origin: z.number().min(0).max(1)
|
|
38
|
+
}).describe("Per-signal consensus contributions before weighting."),
|
|
39
|
+
weights: CompellingnessWeightsSchema,
|
|
40
|
+
scoringVersion: z.string().describe("Methodology version that produced this score.")
|
|
41
|
+
}).meta({ id: "ScoreResult" }).describe("A reference compellingness score for an observation, with a contestedness range.");
|
|
42
|
+
var UNIFORM_TRUST = () => 1;
|
|
43
|
+
function isProsaic(id, prefixes) {
|
|
44
|
+
return prefixes.some((p) => id === p || id.startsWith(`${p}.`));
|
|
45
|
+
}
|
|
46
|
+
function round(n) {
|
|
47
|
+
return Math.round(n * 1e4) / 1e4;
|
|
48
|
+
}
|
|
49
|
+
var EMPTY_STATS = { point: 0, low: 0, high: 0, contested: false };
|
|
50
|
+
function reduceClaims(weighted) {
|
|
51
|
+
if (weighted.length === 0) return EMPTY_STATS;
|
|
52
|
+
let wsum = 0;
|
|
53
|
+
let msum = 0;
|
|
54
|
+
let low = Infinity;
|
|
55
|
+
let high = -Infinity;
|
|
56
|
+
let anyAnomaly = false;
|
|
57
|
+
let anyNone = false;
|
|
58
|
+
for (const { magnitude, weight } of weighted) {
|
|
59
|
+
wsum += weight;
|
|
60
|
+
msum += magnitude * weight;
|
|
61
|
+
if (magnitude < low) low = magnitude;
|
|
62
|
+
if (magnitude > high) high = magnitude;
|
|
63
|
+
if (magnitude > 0) anyAnomaly = true;
|
|
64
|
+
else anyNone = true;
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
point: wsum > 0 ? msum / wsum : 0,
|
|
68
|
+
low,
|
|
69
|
+
high,
|
|
70
|
+
contested: anyAnomaly && anyNone
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function observableMagnitude(claim) {
|
|
74
|
+
return ASSESSMENT_LEVEL_WEIGHTS[claim.level] * (claim.confidence ?? 1);
|
|
75
|
+
}
|
|
76
|
+
function domainStats(group, evaluatorWeight) {
|
|
77
|
+
if (!group) return EMPTY_STATS;
|
|
78
|
+
let point = 0;
|
|
79
|
+
let low = 0;
|
|
80
|
+
let high = 0;
|
|
81
|
+
let contested = false;
|
|
82
|
+
for (const claims of Object.values(group)) {
|
|
83
|
+
if (!claims || claims.length === 0) continue;
|
|
84
|
+
const weighted = claims.map((c) => ({
|
|
85
|
+
magnitude: observableMagnitude(c),
|
|
86
|
+
weight: (c.confidence ?? 1) * evaluatorWeight(c)
|
|
87
|
+
}));
|
|
88
|
+
const stats = reduceClaims(weighted);
|
|
89
|
+
if (stats.point > point) point = stats.point;
|
|
90
|
+
if (stats.low > low) low = stats.low;
|
|
91
|
+
if (stats.high > high) high = stats.high;
|
|
92
|
+
if (stats.contested) contested = true;
|
|
93
|
+
}
|
|
94
|
+
return { point, low, high, contested };
|
|
95
|
+
}
|
|
96
|
+
function originStats(origin, prefixes, evaluatorWeight) {
|
|
97
|
+
if (!origin || origin.length === 0) return EMPTY_STATS;
|
|
98
|
+
const weighted = origin.map((c) => ({
|
|
99
|
+
magnitude: isProsaic(c.primaryHypothesis, prefixes) ? 0 : c.confidence,
|
|
100
|
+
weight: c.confidence * evaluatorWeight(c)
|
|
101
|
+
}));
|
|
102
|
+
return reduceClaims(weighted);
|
|
103
|
+
}
|
|
104
|
+
function score(observation, options) {
|
|
105
|
+
const weights = options?.weights ?? DEFAULT_WEIGHTS;
|
|
106
|
+
const prosaicPrefixes = options?.prosaicPrefixes ?? DEFAULT_PROSAIC_OCS_PREFIXES;
|
|
107
|
+
const evaluatorWeight = options?.evaluatorWeight ?? UNIFORM_TRUST;
|
|
108
|
+
const assessments = observation.observableAssessments;
|
|
109
|
+
const technology = domainStats(assessments?.technology, evaluatorWeight);
|
|
110
|
+
const biologics = domainStats(assessments?.biologics, evaluatorWeight);
|
|
111
|
+
const origin = originStats(observation.origin, prosaicPrefixes, evaluatorWeight);
|
|
112
|
+
const totalWeight = weights.technology + weights.biologics + weights.origin;
|
|
113
|
+
const combine = (t, b, o) => totalWeight > 0 ? (t * weights.technology + b * weights.biologics + o * weights.origin) / totalWeight : 0;
|
|
114
|
+
return {
|
|
115
|
+
score: round(combine(technology.point, biologics.point, origin.point)),
|
|
116
|
+
range: {
|
|
117
|
+
low: round(combine(technology.low, biologics.low, origin.low)),
|
|
118
|
+
high: round(combine(technology.high, biologics.high, origin.high))
|
|
119
|
+
},
|
|
120
|
+
contested: technology.contested || biologics.contested || origin.contested,
|
|
121
|
+
components: {
|
|
122
|
+
technology: round(technology.point),
|
|
123
|
+
biologics: round(biologics.point),
|
|
124
|
+
origin: round(origin.point)
|
|
125
|
+
},
|
|
126
|
+
weights,
|
|
127
|
+
scoringVersion: SCORING_VERSION
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function rankByCompellingness(observations, options) {
|
|
131
|
+
return observations.map((observation) => ({ observation, result: score(observation, options) })).sort((a, b) => b.result.score - a.result.score);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export { ASSESSMENT_LEVEL_WEIGHTS, CompellingnessWeightsSchema, DEFAULT_PROSAIC_OCS_PREFIXES, DEFAULT_WEIGHTS, SCORING_VERSION, ScoreResultSchema, rankByCompellingness, score };
|
|
135
|
+
//# sourceMappingURL=index.js.map
|
|
136
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/compellingness/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import { z } from 'zod';\nimport type { Observation } from '@disclosureos/records';\nimport type { AssessmentLevel, ObservableClaim } from '@disclosureos/observables';\nimport type { OriginClaim } from '@disclosureos/origins';\n// Side-effect imports register the `observableAssessments` and `origin` slots on\n// `Observation` via module augmentation, so they are typed below.\nimport '@disclosureos/observables';\nimport '@disclosureos/origins';\n\n/** Methodology version stamped on every {@link ScoreResult}. Bump on weight/formula changes. */\nexport const SCORING_VERSION = '2.0.0' as const;\n\n/**\n * Ordinal anomaly strength per evidentiary tier. A `confirmed` signal counts\n * fully; `not_indicated` contributes nothing. Multiplied by the assessment's\n * confidence (default 1 when unstated). Exported so consumers can render the\n * methodology; override semantics may come in a future minor release.\n */\nexport const ASSESSMENT_LEVEL_WEIGHTS: Record<AssessmentLevel, number> = {\n not_indicated: 0,\n reported: 0.25,\n documented: 0.5,\n measured: 0.75,\n confirmed: 1,\n};\n\n/**\n * OCS id prefixes treated as prosaic (well-explained). A confident *prosaic*\n * classification means the case is explained, so it contributes no anomaly\n * signal. Overridable per {@link score} call. Default is the OCS \"Prosaic\"\n * branch (`1.1.1`).\n */\nexport const DEFAULT_PROSAIC_OCS_PREFIXES: readonly string[] = ['1.1.1'];\n\nexport const CompellingnessWeightsSchema = z\n .object({\n technology: z.number().min(0).describe('Weight of the technology-anomaly signal.'),\n biologics: z.number().min(0).describe('Weight of the biologics-anomaly signal.'),\n origin: z.number().min(0).describe('Weight of the (non-prosaic) origin-confidence signal.'),\n })\n .meta({ id: 'CompellingnessWeights' })\n .describe('Relative weights for the compellingness components. Need not sum to 1 — normalized internally.');\n\nexport type CompellingnessWeights = z.infer<typeof CompellingnessWeightsSchema>;\n\n/**\n * Fixed default weights, exposed as a transparent, overridable data object (not a\n * config system). Partners can propose alternate weight sets without an API change.\n *\n * @experimental The *shape* (`CompellingnessWeights`) is stable, but these specific\n * default values are a reference methodology that may be re-tuned in a minor release.\n * Pin your own `weights` via {@link ScoreOptions} if you need stable numbers.\n */\nexport const DEFAULT_WEIGHTS: CompellingnessWeights = {\n technology: 0.45,\n biologics: 0.35,\n origin: 0.2,\n};\n\nexport const ScoreResultSchema = z\n .object({\n score: z.number().min(0).max(1).describe('Overall compellingness (consensus point estimate), 0–1.'),\n range: z\n .object({\n low: z.number().min(0).max(1).describe('Most-skeptical reading of the strongest-anomaly signal across competing claims.'),\n high: z.number().min(0).max(1).describe('Most-anomalous reading of the strongest-anomaly signal across competing claims.'),\n })\n .describe(\n 'Spread of the score across competing claims — wide range signals disagreement. Bounds are '\n + 'computed per-signal under the same strongest-anomaly (max) reduction as the point estimate, '\n + 'not a global min/max over every claim.',\n ),\n contested: z\n .boolean()\n .describe('True when claims disagree on *direction* (some assert anomaly, some assert none).'),\n components: z\n .object({\n technology: z.number().min(0).max(1),\n biologics: z.number().min(0).max(1),\n origin: z.number().min(0).max(1),\n })\n .describe('Per-signal consensus contributions before weighting.'),\n weights: CompellingnessWeightsSchema,\n scoringVersion: z.string().describe('Methodology version that produced this score.'),\n })\n .meta({ id: 'ScoreResult' })\n .describe('A reference compellingness score for an observation, with a contestedness range.');\n\nexport type ScoreResult = z.infer<typeof ScoreResultSchema>;\n\n/** The minimal claim shape the trust hook sees (it never needs the full body). */\nexport type EvaluatorWeightInput = {\n evaluatedBy?: string | undefined;\n evidenceRefs?: string[] | undefined;\n};\n\nexport interface ScoreOptions {\n weights?: CompellingnessWeights;\n /** Override the prosaic OCS prefixes (defaults to {@link DEFAULT_PROSAIC_OCS_PREFIXES}). */\n prosaicPrefixes?: readonly string[];\n /**\n * Trust applied to each claim's vote in the consensus point estimate (default\n * 1.0 — uniform). This is where a future trust policy (e.g.\n * weighting verified/official evaluators higher) plugs in here without changing\n * the formula. Range/contestedness are NOT affected — they report the honest\n * spread of what evaluators actually asserted.\n *\n * @experimental The hook signature is stable, but the default trust *policy*\n * (uniform 1.0) is deliberately unopinionated and is a separate, ongoing design\n * task; a future built-in policy may change the default.\n */\n evaluatorWeight?: (claim: EvaluatorWeightInput) => number;\n}\n\nconst UNIFORM_TRUST = (): number => 1;\n\nfunction isProsaic(id: string, prefixes: readonly string[]): boolean {\n return prefixes.some((p) => id === p || id.startsWith(`${p}.`));\n}\n\nfunction round(n: number): number {\n return Math.round(n * 1e4) / 1e4;\n}\n\ntype Weighted = { magnitude: number; weight: number };\n\ninterface ClaimStats {\n /** Trust-weighted mean magnitude (the consensus point). */\n point: number;\n /** Min asserted magnitude (most skeptical). */\n low: number;\n /** Max asserted magnitude (most anomalous). */\n high: number;\n /** Claims disagree on direction (some > 0, some === 0). */\n contested: boolean;\n}\n\nconst EMPTY_STATS: ClaimStats = { point: 0, low: 0, high: 0, contested: false };\n\nfunction reduceClaims(weighted: Weighted[]): ClaimStats {\n if (weighted.length === 0) return EMPTY_STATS;\n let wsum = 0;\n let msum = 0;\n let low = Infinity;\n let high = -Infinity;\n let anyAnomaly = false;\n let anyNone = false;\n for (const { magnitude, weight } of weighted) {\n wsum += weight;\n msum += magnitude * weight;\n if (magnitude < low) low = magnitude;\n if (magnitude > high) high = magnitude;\n if (magnitude > 0) anyAnomaly = true;\n else anyNone = true;\n }\n return {\n point: wsum > 0 ? msum / wsum : 0,\n low,\n high,\n contested: anyAnomaly && anyNone,\n };\n}\n\n/** Asserted anomaly magnitude of one observable claim, in [0,1]. */\nfunction observableMagnitude(claim: ObservableClaim): number {\n return ASSESSMENT_LEVEL_WEIGHTS[claim.level] * (claim.confidence ?? 1);\n}\n\n/**\n * Strongest established anomaly within a domain — the \"is there *any* anomaly?\" reduction.\n * `point`/`low`/`high` are each a **max across observables** of that observable's claim-list\n * stat. So `low` is \"the most-skeptical reading of the strongest-anomaly observable,\" not a\n * global floor across all observables — consistent with the point estimate's max reduction.\n */\nfunction domainStats(\n group: Partial<Record<string, ObservableClaim[]>> | undefined,\n evaluatorWeight: (claim: EvaluatorWeightInput) => number,\n): ClaimStats {\n if (!group) return EMPTY_STATS;\n let point = 0;\n let low = 0;\n let high = 0;\n let contested = false;\n for (const claims of Object.values(group)) {\n if (!claims || claims.length === 0) continue;\n const weighted = claims.map<Weighted>((c) => ({\n magnitude: observableMagnitude(c),\n weight: (c.confidence ?? 1) * evaluatorWeight(c),\n }));\n const stats = reduceClaims(weighted);\n if (stats.point > point) point = stats.point;\n if (stats.low > low) low = stats.low;\n if (stats.high > high) high = stats.high;\n if (stats.contested) contested = true;\n }\n return { point, low, high, contested };\n}\n\n/** Confidence in a *non-prosaic* origin, reduced across competing classifications. */\nfunction originStats(\n origin: OriginClaim[] | undefined,\n prefixes: readonly string[],\n evaluatorWeight: (claim: EvaluatorWeightInput) => number,\n): ClaimStats {\n if (!origin || origin.length === 0) return EMPTY_STATS;\n const weighted = origin.map<Weighted>((c) => ({\n magnitude: isProsaic(c.primaryHypothesis, prefixes) ? 0 : c.confidence,\n weight: c.confidence * evaluatorWeight(c),\n }));\n return reduceClaims(weighted);\n}\n\n/**\n * Reference compellingness scorer. Combines the strongest technology- and\n * biologics-anomaly signals (from `observableAssessments`) with the confidence in\n * a *non-prosaic* origin classification (from `origin`), under transparent,\n * overridable weights. Each slot is a *list* of competing claims, so the scorer\n * reports a consensus `score`, the `range` of readings across claims, and whether\n * the case is `contested` (evaluators disagree on direction). This answers \"how\n * strongly does this case bear on the two public questions — anomalous\n * tech/biologics, and a non-mundane origin?\" — distinct from completeness.\n */\nexport function score(observation: Observation, options?: ScoreOptions): ScoreResult {\n const weights = options?.weights ?? DEFAULT_WEIGHTS;\n const prosaicPrefixes = options?.prosaicPrefixes ?? DEFAULT_PROSAIC_OCS_PREFIXES;\n const evaluatorWeight = options?.evaluatorWeight ?? UNIFORM_TRUST;\n\n const assessments = observation.observableAssessments;\n const technology = domainStats(assessments?.technology, evaluatorWeight);\n const biologics = domainStats(assessments?.biologics, evaluatorWeight);\n const origin = originStats(observation.origin, prosaicPrefixes, evaluatorWeight);\n\n const totalWeight = weights.technology + weights.biologics + weights.origin;\n const combine = (t: number, b: number, o: number): number =>\n totalWeight > 0\n ? (t * weights.technology + b * weights.biologics + o * weights.origin) / totalWeight\n : 0;\n\n return {\n score: round(combine(technology.point, biologics.point, origin.point)),\n range: {\n low: round(combine(technology.low, biologics.low, origin.low)),\n high: round(combine(technology.high, biologics.high, origin.high)),\n },\n contested: technology.contested || biologics.contested || origin.contested,\n components: {\n technology: round(technology.point),\n biologics: round(biologics.point),\n origin: round(origin.point),\n },\n weights,\n scoringVersion: SCORING_VERSION,\n };\n}\n\nexport interface RankedObservation<T extends Observation = Observation> {\n observation: T;\n result: ScoreResult;\n}\n\n/**\n * Score a set of observations and return them sorted most-compelling first — the\n * \"float the strongest cases to the top\" use case.\n */\nexport function rankByCompellingness<T extends Observation>(\n observations: readonly T[],\n options?: ScoreOptions,\n): RankedObservation<T>[] {\n return observations\n .map((observation) => ({ observation, result: score(observation, options) }))\n .sort((a, b) => b.result.score - a.result.score);\n}\n"]}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { Observation } from '@disclosureos/records';
|
|
3
|
+
|
|
4
|
+
/** A leaf field path derived from the records JSON Schema. */
|
|
5
|
+
interface FieldPath {
|
|
6
|
+
/** Dot-path into an `Observation` (e.g. `location.latitude`). */
|
|
7
|
+
path: string;
|
|
8
|
+
/** True only when every segment along the path is schema-required. */
|
|
9
|
+
required: boolean;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Walk the committed records JSON Schema and enumerate every leaf field path on
|
|
13
|
+
* `Observation`. Objects are recursed (resolving `$ref` into `$defs`); arrays,
|
|
14
|
+
* enums, scalars, and `record`/`anyOf` nodes are treated as leaves. This is the
|
|
15
|
+
* single source of completeness coverage — there is no hand-maintained path list
|
|
16
|
+
* (so adding a field to the schema automatically extends completeness).
|
|
17
|
+
*/
|
|
18
|
+
declare function deriveFieldPaths(schema?: Record<string, unknown>): FieldPath[];
|
|
19
|
+
|
|
20
|
+
declare const CompletenessResultSchema: z.ZodObject<{
|
|
21
|
+
total: z.ZodNumber;
|
|
22
|
+
present: z.ZodNumber;
|
|
23
|
+
percentage: z.ZodNumber;
|
|
24
|
+
missing: z.ZodArray<z.ZodString>;
|
|
25
|
+
requiredTotal: z.ZodNumber;
|
|
26
|
+
requiredPresent: z.ZodNumber;
|
|
27
|
+
requiredPercentage: z.ZodNumber;
|
|
28
|
+
}, z.core.$strip>;
|
|
29
|
+
type CompletenessResult = z.infer<typeof CompletenessResultSchema>;
|
|
30
|
+
interface CompletenessOptions {
|
|
31
|
+
/** Override the derived field-path set (defaults to the records schema). */
|
|
32
|
+
paths?: FieldPath[];
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Measure how fully an observation populates the records schema. This is the
|
|
36
|
+
* "is it well-documented?" axis — deliberately kept separate from compellingness
|
|
37
|
+
* ("is it anomalous?"), so a thin-but-anomalous case never masquerades as complete.
|
|
38
|
+
*/
|
|
39
|
+
declare function getCompleteness(observation: Observation | Record<string, unknown>, options?: CompletenessOptions): CompletenessResult;
|
|
40
|
+
|
|
41
|
+
export { type CompletenessOptions, type CompletenessResult, CompletenessResultSchema, type FieldPath, deriveFieldPaths, getCompleteness };
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { recordsJsonSchema } from '@disclosureos/records';
|
|
3
|
+
|
|
4
|
+
// src/completeness/index.ts
|
|
5
|
+
function refName(ref) {
|
|
6
|
+
return ref.replace("#/$defs/", "");
|
|
7
|
+
}
|
|
8
|
+
function deriveFieldPaths(schema) {
|
|
9
|
+
const root = schema ?? recordsJsonSchema();
|
|
10
|
+
const defs = root.$defs ?? {};
|
|
11
|
+
const out = [];
|
|
12
|
+
function resolve(node) {
|
|
13
|
+
let cur = node;
|
|
14
|
+
const seen = /* @__PURE__ */ new Set();
|
|
15
|
+
while (cur.$ref) {
|
|
16
|
+
const name = refName(cur.$ref);
|
|
17
|
+
if (seen.has(name)) break;
|
|
18
|
+
seen.add(name);
|
|
19
|
+
cur = defs[name] ?? {};
|
|
20
|
+
}
|
|
21
|
+
return cur;
|
|
22
|
+
}
|
|
23
|
+
function walk(node, prefix, required, visited) {
|
|
24
|
+
const refn = node.$ref ? refName(node.$ref) : void 0;
|
|
25
|
+
if (refn && visited.has(refn)) {
|
|
26
|
+
if (prefix) out.push({ path: prefix, required });
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const resolved = resolve(node);
|
|
30
|
+
const props = resolved.properties;
|
|
31
|
+
if (props && Object.keys(props).length > 0) {
|
|
32
|
+
const nextVisited = refn ? /* @__PURE__ */ new Set([...visited, refn]) : visited;
|
|
33
|
+
const req = new Set(resolved.required ?? []);
|
|
34
|
+
for (const [key, child] of Object.entries(props)) {
|
|
35
|
+
const childPath = prefix ? `${prefix}.${key}` : key;
|
|
36
|
+
walk(child, childPath, required && req.has(key), nextVisited);
|
|
37
|
+
}
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (prefix) out.push({ path: prefix, required });
|
|
41
|
+
}
|
|
42
|
+
if (root.$ref) {
|
|
43
|
+
walk({ $ref: root.$ref }, "", true, /* @__PURE__ */ new Set());
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// src/completeness/index.ts
|
|
49
|
+
var CompletenessResultSchema = z.object({
|
|
50
|
+
total: z.number().int().describe("Total leaf field paths defined by the records schema."),
|
|
51
|
+
present: z.number().int().describe("How many of those paths carry a value."),
|
|
52
|
+
percentage: z.number().int().describe("present / total, as a 0\u2013100 integer."),
|
|
53
|
+
missing: z.array(z.string()).describe("Paths that are absent or empty."),
|
|
54
|
+
requiredTotal: z.number().int().describe("Count of schema-required leaf paths."),
|
|
55
|
+
requiredPresent: z.number().int().describe("How many required paths carry a value."),
|
|
56
|
+
requiredPercentage: z.number().int().describe("requiredPresent / requiredTotal, as a 0\u2013100 integer.")
|
|
57
|
+
}).meta({ id: "CompletenessResult" }).describe("How fully an observation populates the records schema.");
|
|
58
|
+
function getNested(obj, path) {
|
|
59
|
+
let current = obj;
|
|
60
|
+
for (const part of path.split(".")) {
|
|
61
|
+
if (current == null || typeof current !== "object") return void 0;
|
|
62
|
+
current = current[part];
|
|
63
|
+
}
|
|
64
|
+
return current;
|
|
65
|
+
}
|
|
66
|
+
function isPresent(value) {
|
|
67
|
+
if (value == null) return false;
|
|
68
|
+
if (typeof value === "string") return value.length > 0;
|
|
69
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
70
|
+
if (typeof value === "object") return Object.keys(value).length > 0;
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
var cachedPaths = null;
|
|
74
|
+
function defaultPaths() {
|
|
75
|
+
return cachedPaths ?? (cachedPaths = deriveFieldPaths());
|
|
76
|
+
}
|
|
77
|
+
function pct(present, total) {
|
|
78
|
+
return total > 0 ? Math.round(present / total * 100) : 0;
|
|
79
|
+
}
|
|
80
|
+
function getCompleteness(observation, options) {
|
|
81
|
+
const record = observation;
|
|
82
|
+
const paths = options?.paths ?? defaultPaths();
|
|
83
|
+
const missing = [];
|
|
84
|
+
let present = 0;
|
|
85
|
+
let requiredTotal = 0;
|
|
86
|
+
let requiredPresent = 0;
|
|
87
|
+
for (const field of paths) {
|
|
88
|
+
const ok = isPresent(getNested(record, field.path));
|
|
89
|
+
if (ok) present++;
|
|
90
|
+
else missing.push(field.path);
|
|
91
|
+
if (field.required) {
|
|
92
|
+
requiredTotal++;
|
|
93
|
+
if (ok) requiredPresent++;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
total: paths.length,
|
|
98
|
+
present,
|
|
99
|
+
percentage: pct(present, paths.length),
|
|
100
|
+
missing,
|
|
101
|
+
requiredTotal,
|
|
102
|
+
requiredPresent,
|
|
103
|
+
requiredPercentage: pct(requiredPresent, requiredTotal)
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export { CompletenessResultSchema, deriveFieldPaths, getCompleteness };
|
|
108
|
+
//# sourceMappingURL=index.js.map
|
|
109
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/completeness/field-paths.ts","../../src/completeness/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import { recordsJsonSchema } from '@disclosureos/records';\n\n/** A leaf field path derived from the records JSON Schema. */\nexport interface FieldPath {\n /** Dot-path into an `Observation` (e.g. `location.latitude`). */\n path: string;\n /** True only when every segment along the path is schema-required. */\n required: boolean;\n}\n\ninterface SchemaNode {\n $ref?: string;\n properties?: Record<string, SchemaNode>;\n required?: string[];\n type?: string | string[];\n}\n\nfunction refName(ref: string): string {\n return ref.replace('#/$defs/', '');\n}\n\n/**\n * Walk the committed records JSON Schema and enumerate every leaf field path on\n * `Observation`. Objects are recursed (resolving `$ref` into `$defs`); arrays,\n * enums, scalars, and `record`/`anyOf` nodes are treated as leaves. This is the\n * single source of completeness coverage — there is no hand-maintained path list\n * (so adding a field to the schema automatically extends completeness).\n */\nexport function deriveFieldPaths(schema?: Record<string, unknown>): FieldPath[] {\n const root = (schema ?? recordsJsonSchema()) as Record<string, unknown> & {\n $ref?: string;\n $defs?: Record<string, SchemaNode>;\n };\n const defs = root.$defs ?? {};\n const out: FieldPath[] = [];\n\n function resolve(node: SchemaNode): SchemaNode {\n let cur: SchemaNode = node;\n const seen = new Set<string>();\n while (cur.$ref) {\n const name = refName(cur.$ref);\n if (seen.has(name)) break;\n seen.add(name);\n cur = defs[name] ?? {};\n }\n return cur;\n }\n\n function walk(node: SchemaNode, prefix: string, required: boolean, visited: Set<string>): void {\n const refn = node.$ref ? refName(node.$ref) : undefined;\n // Cycle guard: a recursive $ref re-entering an in-progress def becomes a leaf.\n if (refn && visited.has(refn)) {\n if (prefix) out.push({ path: prefix, required });\n return;\n }\n const resolved = resolve(node);\n const props = resolved.properties;\n if (props && Object.keys(props).length > 0) {\n const nextVisited = refn ? new Set([...visited, refn]) : visited;\n const req = new Set(resolved.required ?? []);\n for (const [key, child] of Object.entries(props)) {\n const childPath = prefix ? `${prefix}.${key}` : key;\n walk(child, childPath, required && req.has(key), nextVisited);\n }\n return;\n }\n if (prefix) out.push({ path: prefix, required });\n }\n\n if (root.$ref) {\n walk({ $ref: root.$ref }, '', true, new Set());\n }\n return out;\n}\n","import { z } from 'zod';\nimport type { Observation } from '@disclosureos/records';\nimport { deriveFieldPaths } from './field-paths';\nimport type { FieldPath } from './field-paths';\n\nexport type { FieldPath } from './field-paths';\nexport { deriveFieldPaths } from './field-paths';\n\nexport const CompletenessResultSchema = z\n .object({\n total: z.number().int().describe('Total leaf field paths defined by the records schema.'),\n present: z.number().int().describe('How many of those paths carry a value.'),\n percentage: z.number().int().describe('present / total, as a 0–100 integer.'),\n missing: z.array(z.string()).describe('Paths that are absent or empty.'),\n requiredTotal: z.number().int().describe('Count of schema-required leaf paths.'),\n requiredPresent: z.number().int().describe('How many required paths carry a value.'),\n requiredPercentage: z\n .number()\n .int()\n .describe('requiredPresent / requiredTotal, as a 0–100 integer.'),\n })\n .meta({ id: 'CompletenessResult' })\n .describe('How fully an observation populates the records schema.');\n\nexport type CompletenessResult = z.infer<typeof CompletenessResultSchema>;\n\nexport interface CompletenessOptions {\n /** Override the derived field-path set (defaults to the records schema). */\n paths?: FieldPath[];\n}\n\nfunction getNested(obj: Record<string, unknown>, path: string): unknown {\n let current: unknown = obj;\n for (const part of path.split('.')) {\n if (current == null || typeof current !== 'object') return undefined;\n current = (current as Record<string, unknown>)[part];\n }\n return current;\n}\n\nfunction isPresent(value: unknown): boolean {\n if (value == null) return false;\n if (typeof value === 'string') return value.length > 0;\n if (Array.isArray(value)) return value.length > 0;\n if (typeof value === 'object') return Object.keys(value).length > 0;\n return true;\n}\n\n// Field paths are stable for a given schema version, so derive once and reuse.\nlet cachedPaths: FieldPath[] | null = null;\nfunction defaultPaths(): FieldPath[] {\n return (cachedPaths ??= deriveFieldPaths());\n}\n\nfunction pct(present: number, total: number): number {\n return total > 0 ? Math.round((present / total) * 100) : 0;\n}\n\n/**\n * Measure how fully an observation populates the records schema. This is the\n * \"is it well-documented?\" axis — deliberately kept separate from compellingness\n * (\"is it anomalous?\"), so a thin-but-anomalous case never masquerades as complete.\n */\nexport function getCompleteness(\n observation: Observation | Record<string, unknown>,\n options?: CompletenessOptions,\n): CompletenessResult {\n const record = observation as Record<string, unknown>;\n const paths = options?.paths ?? defaultPaths();\n const missing: string[] = [];\n let present = 0;\n let requiredTotal = 0;\n let requiredPresent = 0;\n\n for (const field of paths) {\n const ok = isPresent(getNested(record, field.path));\n if (ok) present++;\n else missing.push(field.path);\n if (field.required) {\n requiredTotal++;\n if (ok) requiredPresent++;\n }\n }\n\n return {\n total: paths.length,\n present,\n percentage: pct(present, paths.length),\n missing,\n requiredTotal,\n requiredPresent,\n requiredPercentage: pct(requiredPresent, requiredTotal),\n };\n}\n"]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export { CompletenessOptions, CompletenessResult, CompletenessResultSchema, FieldPath, deriveFieldPaths, getCompleteness } from './completeness/index.js';
|
|
2
|
+
export { ASSESSMENT_LEVEL_WEIGHTS, CompellingnessWeights, CompellingnessWeightsSchema, DEFAULT_PROSAIC_OCS_PREFIXES, DEFAULT_WEIGHTS, EvaluatorWeightInput, RankedObservation, SCORING_VERSION, ScoreOptions, ScoreResult, ScoreResultSchema, rankByCompellingness, score } from './compellingness/index.js';
|
|
3
|
+
import 'zod';
|
|
4
|
+
import '@disclosureos/records';
|
|
5
|
+
import '@disclosureos/observables';
|
|
6
|
+
|
|
7
|
+
declare const SCORING_SCHEMA_VERSION = "2.0.0";
|
|
8
|
+
declare const SCORING_SCHEMA_ID: string;
|
|
9
|
+
/**
|
|
10
|
+
* Emit the JSON Schema (draft 2020-12) for the scoring output shapes
|
|
11
|
+
* ({@link CompletenessResultSchema}, {@link ScoreResultSchema}) so downstream
|
|
12
|
+
* consumers (and other languages) can validate stored scores.
|
|
13
|
+
*/
|
|
14
|
+
declare function scoringJsonSchema(): Record<string, unknown>;
|
|
15
|
+
|
|
16
|
+
export { SCORING_SCHEMA_ID, SCORING_SCHEMA_VERSION, scoringJsonSchema };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { recordsJsonSchema } from '@disclosureos/records';
|
|
3
|
+
import '@disclosureos/observables';
|
|
4
|
+
import '@disclosureos/origins';
|
|
5
|
+
import { schemaId, jsonSchemaEnvelope } from '@disclosureos/records/shared';
|
|
6
|
+
|
|
7
|
+
// src/completeness/index.ts
|
|
8
|
+
function refName(ref) {
|
|
9
|
+
return ref.replace("#/$defs/", "");
|
|
10
|
+
}
|
|
11
|
+
function deriveFieldPaths(schema) {
|
|
12
|
+
const root = schema ?? recordsJsonSchema();
|
|
13
|
+
const defs = root.$defs ?? {};
|
|
14
|
+
const out = [];
|
|
15
|
+
function resolve(node) {
|
|
16
|
+
let cur = node;
|
|
17
|
+
const seen = /* @__PURE__ */ new Set();
|
|
18
|
+
while (cur.$ref) {
|
|
19
|
+
const name = refName(cur.$ref);
|
|
20
|
+
if (seen.has(name)) break;
|
|
21
|
+
seen.add(name);
|
|
22
|
+
cur = defs[name] ?? {};
|
|
23
|
+
}
|
|
24
|
+
return cur;
|
|
25
|
+
}
|
|
26
|
+
function walk(node, prefix, required, visited) {
|
|
27
|
+
const refn = node.$ref ? refName(node.$ref) : void 0;
|
|
28
|
+
if (refn && visited.has(refn)) {
|
|
29
|
+
if (prefix) out.push({ path: prefix, required });
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const resolved = resolve(node);
|
|
33
|
+
const props = resolved.properties;
|
|
34
|
+
if (props && Object.keys(props).length > 0) {
|
|
35
|
+
const nextVisited = refn ? /* @__PURE__ */ new Set([...visited, refn]) : visited;
|
|
36
|
+
const req = new Set(resolved.required ?? []);
|
|
37
|
+
for (const [key, child] of Object.entries(props)) {
|
|
38
|
+
const childPath = prefix ? `${prefix}.${key}` : key;
|
|
39
|
+
walk(child, childPath, required && req.has(key), nextVisited);
|
|
40
|
+
}
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (prefix) out.push({ path: prefix, required });
|
|
44
|
+
}
|
|
45
|
+
if (root.$ref) {
|
|
46
|
+
walk({ $ref: root.$ref }, "", true, /* @__PURE__ */ new Set());
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// src/completeness/index.ts
|
|
52
|
+
var CompletenessResultSchema = z.object({
|
|
53
|
+
total: z.number().int().describe("Total leaf field paths defined by the records schema."),
|
|
54
|
+
present: z.number().int().describe("How many of those paths carry a value."),
|
|
55
|
+
percentage: z.number().int().describe("present / total, as a 0\u2013100 integer."),
|
|
56
|
+
missing: z.array(z.string()).describe("Paths that are absent or empty."),
|
|
57
|
+
requiredTotal: z.number().int().describe("Count of schema-required leaf paths."),
|
|
58
|
+
requiredPresent: z.number().int().describe("How many required paths carry a value."),
|
|
59
|
+
requiredPercentage: z.number().int().describe("requiredPresent / requiredTotal, as a 0\u2013100 integer.")
|
|
60
|
+
}).meta({ id: "CompletenessResult" }).describe("How fully an observation populates the records schema.");
|
|
61
|
+
function getNested(obj, path) {
|
|
62
|
+
let current = obj;
|
|
63
|
+
for (const part of path.split(".")) {
|
|
64
|
+
if (current == null || typeof current !== "object") return void 0;
|
|
65
|
+
current = current[part];
|
|
66
|
+
}
|
|
67
|
+
return current;
|
|
68
|
+
}
|
|
69
|
+
function isPresent(value) {
|
|
70
|
+
if (value == null) return false;
|
|
71
|
+
if (typeof value === "string") return value.length > 0;
|
|
72
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
73
|
+
if (typeof value === "object") return Object.keys(value).length > 0;
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
var cachedPaths = null;
|
|
77
|
+
function defaultPaths() {
|
|
78
|
+
return cachedPaths ?? (cachedPaths = deriveFieldPaths());
|
|
79
|
+
}
|
|
80
|
+
function pct(present, total) {
|
|
81
|
+
return total > 0 ? Math.round(present / total * 100) : 0;
|
|
82
|
+
}
|
|
83
|
+
function getCompleteness(observation, options) {
|
|
84
|
+
const record = observation;
|
|
85
|
+
const paths = options?.paths ?? defaultPaths();
|
|
86
|
+
const missing = [];
|
|
87
|
+
let present = 0;
|
|
88
|
+
let requiredTotal = 0;
|
|
89
|
+
let requiredPresent = 0;
|
|
90
|
+
for (const field of paths) {
|
|
91
|
+
const ok = isPresent(getNested(record, field.path));
|
|
92
|
+
if (ok) present++;
|
|
93
|
+
else missing.push(field.path);
|
|
94
|
+
if (field.required) {
|
|
95
|
+
requiredTotal++;
|
|
96
|
+
if (ok) requiredPresent++;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
total: paths.length,
|
|
101
|
+
present,
|
|
102
|
+
percentage: pct(present, paths.length),
|
|
103
|
+
missing,
|
|
104
|
+
requiredTotal,
|
|
105
|
+
requiredPresent,
|
|
106
|
+
requiredPercentage: pct(requiredPresent, requiredTotal)
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
var SCORING_VERSION = "2.0.0";
|
|
110
|
+
var ASSESSMENT_LEVEL_WEIGHTS = {
|
|
111
|
+
not_indicated: 0,
|
|
112
|
+
reported: 0.25,
|
|
113
|
+
documented: 0.5,
|
|
114
|
+
measured: 0.75,
|
|
115
|
+
confirmed: 1
|
|
116
|
+
};
|
|
117
|
+
var DEFAULT_PROSAIC_OCS_PREFIXES = ["1.1.1"];
|
|
118
|
+
var CompellingnessWeightsSchema = z.object({
|
|
119
|
+
technology: z.number().min(0).describe("Weight of the technology-anomaly signal."),
|
|
120
|
+
biologics: z.number().min(0).describe("Weight of the biologics-anomaly signal."),
|
|
121
|
+
origin: z.number().min(0).describe("Weight of the (non-prosaic) origin-confidence signal.")
|
|
122
|
+
}).meta({ id: "CompellingnessWeights" }).describe("Relative weights for the compellingness components. Need not sum to 1 \u2014 normalized internally.");
|
|
123
|
+
var DEFAULT_WEIGHTS = {
|
|
124
|
+
technology: 0.45,
|
|
125
|
+
biologics: 0.35,
|
|
126
|
+
origin: 0.2
|
|
127
|
+
};
|
|
128
|
+
var ScoreResultSchema = z.object({
|
|
129
|
+
score: z.number().min(0).max(1).describe("Overall compellingness (consensus point estimate), 0\u20131."),
|
|
130
|
+
range: z.object({
|
|
131
|
+
low: z.number().min(0).max(1).describe("Most-skeptical reading of the strongest-anomaly signal across competing claims."),
|
|
132
|
+
high: z.number().min(0).max(1).describe("Most-anomalous reading of the strongest-anomaly signal across competing claims.")
|
|
133
|
+
}).describe(
|
|
134
|
+
"Spread of the score across competing claims \u2014 wide range signals disagreement. Bounds are computed per-signal under the same strongest-anomaly (max) reduction as the point estimate, not a global min/max over every claim."
|
|
135
|
+
),
|
|
136
|
+
contested: z.boolean().describe("True when claims disagree on *direction* (some assert anomaly, some assert none)."),
|
|
137
|
+
components: z.object({
|
|
138
|
+
technology: z.number().min(0).max(1),
|
|
139
|
+
biologics: z.number().min(0).max(1),
|
|
140
|
+
origin: z.number().min(0).max(1)
|
|
141
|
+
}).describe("Per-signal consensus contributions before weighting."),
|
|
142
|
+
weights: CompellingnessWeightsSchema,
|
|
143
|
+
scoringVersion: z.string().describe("Methodology version that produced this score.")
|
|
144
|
+
}).meta({ id: "ScoreResult" }).describe("A reference compellingness score for an observation, with a contestedness range.");
|
|
145
|
+
var UNIFORM_TRUST = () => 1;
|
|
146
|
+
function isProsaic(id, prefixes) {
|
|
147
|
+
return prefixes.some((p) => id === p || id.startsWith(`${p}.`));
|
|
148
|
+
}
|
|
149
|
+
function round(n) {
|
|
150
|
+
return Math.round(n * 1e4) / 1e4;
|
|
151
|
+
}
|
|
152
|
+
var EMPTY_STATS = { point: 0, low: 0, high: 0, contested: false };
|
|
153
|
+
function reduceClaims(weighted) {
|
|
154
|
+
if (weighted.length === 0) return EMPTY_STATS;
|
|
155
|
+
let wsum = 0;
|
|
156
|
+
let msum = 0;
|
|
157
|
+
let low = Infinity;
|
|
158
|
+
let high = -Infinity;
|
|
159
|
+
let anyAnomaly = false;
|
|
160
|
+
let anyNone = false;
|
|
161
|
+
for (const { magnitude, weight } of weighted) {
|
|
162
|
+
wsum += weight;
|
|
163
|
+
msum += magnitude * weight;
|
|
164
|
+
if (magnitude < low) low = magnitude;
|
|
165
|
+
if (magnitude > high) high = magnitude;
|
|
166
|
+
if (magnitude > 0) anyAnomaly = true;
|
|
167
|
+
else anyNone = true;
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
point: wsum > 0 ? msum / wsum : 0,
|
|
171
|
+
low,
|
|
172
|
+
high,
|
|
173
|
+
contested: anyAnomaly && anyNone
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
function observableMagnitude(claim) {
|
|
177
|
+
return ASSESSMENT_LEVEL_WEIGHTS[claim.level] * (claim.confidence ?? 1);
|
|
178
|
+
}
|
|
179
|
+
function domainStats(group, evaluatorWeight) {
|
|
180
|
+
if (!group) return EMPTY_STATS;
|
|
181
|
+
let point = 0;
|
|
182
|
+
let low = 0;
|
|
183
|
+
let high = 0;
|
|
184
|
+
let contested = false;
|
|
185
|
+
for (const claims of Object.values(group)) {
|
|
186
|
+
if (!claims || claims.length === 0) continue;
|
|
187
|
+
const weighted = claims.map((c) => ({
|
|
188
|
+
magnitude: observableMagnitude(c),
|
|
189
|
+
weight: (c.confidence ?? 1) * evaluatorWeight(c)
|
|
190
|
+
}));
|
|
191
|
+
const stats = reduceClaims(weighted);
|
|
192
|
+
if (stats.point > point) point = stats.point;
|
|
193
|
+
if (stats.low > low) low = stats.low;
|
|
194
|
+
if (stats.high > high) high = stats.high;
|
|
195
|
+
if (stats.contested) contested = true;
|
|
196
|
+
}
|
|
197
|
+
return { point, low, high, contested };
|
|
198
|
+
}
|
|
199
|
+
function originStats(origin, prefixes, evaluatorWeight) {
|
|
200
|
+
if (!origin || origin.length === 0) return EMPTY_STATS;
|
|
201
|
+
const weighted = origin.map((c) => ({
|
|
202
|
+
magnitude: isProsaic(c.primaryHypothesis, prefixes) ? 0 : c.confidence,
|
|
203
|
+
weight: c.confidence * evaluatorWeight(c)
|
|
204
|
+
}));
|
|
205
|
+
return reduceClaims(weighted);
|
|
206
|
+
}
|
|
207
|
+
function score(observation, options) {
|
|
208
|
+
const weights = options?.weights ?? DEFAULT_WEIGHTS;
|
|
209
|
+
const prosaicPrefixes = options?.prosaicPrefixes ?? DEFAULT_PROSAIC_OCS_PREFIXES;
|
|
210
|
+
const evaluatorWeight = options?.evaluatorWeight ?? UNIFORM_TRUST;
|
|
211
|
+
const assessments = observation.observableAssessments;
|
|
212
|
+
const technology = domainStats(assessments?.technology, evaluatorWeight);
|
|
213
|
+
const biologics = domainStats(assessments?.biologics, evaluatorWeight);
|
|
214
|
+
const origin = originStats(observation.origin, prosaicPrefixes, evaluatorWeight);
|
|
215
|
+
const totalWeight = weights.technology + weights.biologics + weights.origin;
|
|
216
|
+
const combine = (t, b, o) => totalWeight > 0 ? (t * weights.technology + b * weights.biologics + o * weights.origin) / totalWeight : 0;
|
|
217
|
+
return {
|
|
218
|
+
score: round(combine(technology.point, biologics.point, origin.point)),
|
|
219
|
+
range: {
|
|
220
|
+
low: round(combine(technology.low, biologics.low, origin.low)),
|
|
221
|
+
high: round(combine(technology.high, biologics.high, origin.high))
|
|
222
|
+
},
|
|
223
|
+
contested: technology.contested || biologics.contested || origin.contested,
|
|
224
|
+
components: {
|
|
225
|
+
technology: round(technology.point),
|
|
226
|
+
biologics: round(biologics.point),
|
|
227
|
+
origin: round(origin.point)
|
|
228
|
+
},
|
|
229
|
+
weights,
|
|
230
|
+
scoringVersion: SCORING_VERSION
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
function rankByCompellingness(observations, options) {
|
|
234
|
+
return observations.map((observation) => ({ observation, result: score(observation, options) })).sort((a, b) => b.result.score - a.result.score);
|
|
235
|
+
}
|
|
236
|
+
var SCORING_SCHEMA_VERSION = "2.0.0";
|
|
237
|
+
var SCORING_SCHEMA_ID = schemaId("scoring", "scoring", SCORING_SCHEMA_VERSION);
|
|
238
|
+
function scoringJsonSchema() {
|
|
239
|
+
const bundle = z.toJSONSchema(
|
|
240
|
+
z.object({
|
|
241
|
+
completeness: CompletenessResultSchema,
|
|
242
|
+
compellingness: ScoreResultSchema,
|
|
243
|
+
weights: CompellingnessWeightsSchema
|
|
244
|
+
}),
|
|
245
|
+
{ target: "draft-2020-12" }
|
|
246
|
+
);
|
|
247
|
+
return jsonSchemaEnvelope(bundle, { id: SCORING_SCHEMA_ID, version: SCORING_SCHEMA_VERSION });
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export { ASSESSMENT_LEVEL_WEIGHTS, CompellingnessWeightsSchema, CompletenessResultSchema, DEFAULT_PROSAIC_OCS_PREFIXES, DEFAULT_WEIGHTS, SCORING_SCHEMA_ID, SCORING_SCHEMA_VERSION, SCORING_VERSION, ScoreResultSchema, deriveFieldPaths, getCompleteness, rankByCompellingness, score, scoringJsonSchema };
|
|
251
|
+
//# sourceMappingURL=index.js.map
|
|
252
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/completeness/field-paths.ts","../src/completeness/index.ts","../src/compellingness/index.ts","../src/schema.ts"],"names":["z"],"mappings":"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","file":"index.js","sourcesContent":["import { recordsJsonSchema } from '@disclosureos/records';\n\n/** A leaf field path derived from the records JSON Schema. */\nexport interface FieldPath {\n /** Dot-path into an `Observation` (e.g. `location.latitude`). */\n path: string;\n /** True only when every segment along the path is schema-required. */\n required: boolean;\n}\n\ninterface SchemaNode {\n $ref?: string;\n properties?: Record<string, SchemaNode>;\n required?: string[];\n type?: string | string[];\n}\n\nfunction refName(ref: string): string {\n return ref.replace('#/$defs/', '');\n}\n\n/**\n * Walk the committed records JSON Schema and enumerate every leaf field path on\n * `Observation`. Objects are recursed (resolving `$ref` into `$defs`); arrays,\n * enums, scalars, and `record`/`anyOf` nodes are treated as leaves. This is the\n * single source of completeness coverage — there is no hand-maintained path list\n * (so adding a field to the schema automatically extends completeness).\n */\nexport function deriveFieldPaths(schema?: Record<string, unknown>): FieldPath[] {\n const root = (schema ?? recordsJsonSchema()) as Record<string, unknown> & {\n $ref?: string;\n $defs?: Record<string, SchemaNode>;\n };\n const defs = root.$defs ?? {};\n const out: FieldPath[] = [];\n\n function resolve(node: SchemaNode): SchemaNode {\n let cur: SchemaNode = node;\n const seen = new Set<string>();\n while (cur.$ref) {\n const name = refName(cur.$ref);\n if (seen.has(name)) break;\n seen.add(name);\n cur = defs[name] ?? {};\n }\n return cur;\n }\n\n function walk(node: SchemaNode, prefix: string, required: boolean, visited: Set<string>): void {\n const refn = node.$ref ? refName(node.$ref) : undefined;\n // Cycle guard: a recursive $ref re-entering an in-progress def becomes a leaf.\n if (refn && visited.has(refn)) {\n if (prefix) out.push({ path: prefix, required });\n return;\n }\n const resolved = resolve(node);\n const props = resolved.properties;\n if (props && Object.keys(props).length > 0) {\n const nextVisited = refn ? new Set([...visited, refn]) : visited;\n const req = new Set(resolved.required ?? []);\n for (const [key, child] of Object.entries(props)) {\n const childPath = prefix ? `${prefix}.${key}` : key;\n walk(child, childPath, required && req.has(key), nextVisited);\n }\n return;\n }\n if (prefix) out.push({ path: prefix, required });\n }\n\n if (root.$ref) {\n walk({ $ref: root.$ref }, '', true, new Set());\n }\n return out;\n}\n","import { z } from 'zod';\nimport type { Observation } from '@disclosureos/records';\nimport { deriveFieldPaths } from './field-paths';\nimport type { FieldPath } from './field-paths';\n\nexport type { FieldPath } from './field-paths';\nexport { deriveFieldPaths } from './field-paths';\n\nexport const CompletenessResultSchema = z\n .object({\n total: z.number().int().describe('Total leaf field paths defined by the records schema.'),\n present: z.number().int().describe('How many of those paths carry a value.'),\n percentage: z.number().int().describe('present / total, as a 0–100 integer.'),\n missing: z.array(z.string()).describe('Paths that are absent or empty.'),\n requiredTotal: z.number().int().describe('Count of schema-required leaf paths.'),\n requiredPresent: z.number().int().describe('How many required paths carry a value.'),\n requiredPercentage: z\n .number()\n .int()\n .describe('requiredPresent / requiredTotal, as a 0–100 integer.'),\n })\n .meta({ id: 'CompletenessResult' })\n .describe('How fully an observation populates the records schema.');\n\nexport type CompletenessResult = z.infer<typeof CompletenessResultSchema>;\n\nexport interface CompletenessOptions {\n /** Override the derived field-path set (defaults to the records schema). */\n paths?: FieldPath[];\n}\n\nfunction getNested(obj: Record<string, unknown>, path: string): unknown {\n let current: unknown = obj;\n for (const part of path.split('.')) {\n if (current == null || typeof current !== 'object') return undefined;\n current = (current as Record<string, unknown>)[part];\n }\n return current;\n}\n\nfunction isPresent(value: unknown): boolean {\n if (value == null) return false;\n if (typeof value === 'string') return value.length > 0;\n if (Array.isArray(value)) return value.length > 0;\n if (typeof value === 'object') return Object.keys(value).length > 0;\n return true;\n}\n\n// Field paths are stable for a given schema version, so derive once and reuse.\nlet cachedPaths: FieldPath[] | null = null;\nfunction defaultPaths(): FieldPath[] {\n return (cachedPaths ??= deriveFieldPaths());\n}\n\nfunction pct(present: number, total: number): number {\n return total > 0 ? Math.round((present / total) * 100) : 0;\n}\n\n/**\n * Measure how fully an observation populates the records schema. This is the\n * \"is it well-documented?\" axis — deliberately kept separate from compellingness\n * (\"is it anomalous?\"), so a thin-but-anomalous case never masquerades as complete.\n */\nexport function getCompleteness(\n observation: Observation | Record<string, unknown>,\n options?: CompletenessOptions,\n): CompletenessResult {\n const record = observation as Record<string, unknown>;\n const paths = options?.paths ?? defaultPaths();\n const missing: string[] = [];\n let present = 0;\n let requiredTotal = 0;\n let requiredPresent = 0;\n\n for (const field of paths) {\n const ok = isPresent(getNested(record, field.path));\n if (ok) present++;\n else missing.push(field.path);\n if (field.required) {\n requiredTotal++;\n if (ok) requiredPresent++;\n }\n }\n\n return {\n total: paths.length,\n present,\n percentage: pct(present, paths.length),\n missing,\n requiredTotal,\n requiredPresent,\n requiredPercentage: pct(requiredPresent, requiredTotal),\n };\n}\n","import { z } from 'zod';\nimport type { Observation } from '@disclosureos/records';\nimport type { AssessmentLevel, ObservableClaim } from '@disclosureos/observables';\nimport type { OriginClaim } from '@disclosureos/origins';\n// Side-effect imports register the `observableAssessments` and `origin` slots on\n// `Observation` via module augmentation, so they are typed below.\nimport '@disclosureos/observables';\nimport '@disclosureos/origins';\n\n/** Methodology version stamped on every {@link ScoreResult}. Bump on weight/formula changes. */\nexport const SCORING_VERSION = '2.0.0' as const;\n\n/**\n * Ordinal anomaly strength per evidentiary tier. A `confirmed` signal counts\n * fully; `not_indicated` contributes nothing. Multiplied by the assessment's\n * confidence (default 1 when unstated). Exported so consumers can render the\n * methodology; override semantics may come in a future minor release.\n */\nexport const ASSESSMENT_LEVEL_WEIGHTS: Record<AssessmentLevel, number> = {\n not_indicated: 0,\n reported: 0.25,\n documented: 0.5,\n measured: 0.75,\n confirmed: 1,\n};\n\n/**\n * OCS id prefixes treated as prosaic (well-explained). A confident *prosaic*\n * classification means the case is explained, so it contributes no anomaly\n * signal. Overridable per {@link score} call. Default is the OCS \"Prosaic\"\n * branch (`1.1.1`).\n */\nexport const DEFAULT_PROSAIC_OCS_PREFIXES: readonly string[] = ['1.1.1'];\n\nexport const CompellingnessWeightsSchema = z\n .object({\n technology: z.number().min(0).describe('Weight of the technology-anomaly signal.'),\n biologics: z.number().min(0).describe('Weight of the biologics-anomaly signal.'),\n origin: z.number().min(0).describe('Weight of the (non-prosaic) origin-confidence signal.'),\n })\n .meta({ id: 'CompellingnessWeights' })\n .describe('Relative weights for the compellingness components. Need not sum to 1 — normalized internally.');\n\nexport type CompellingnessWeights = z.infer<typeof CompellingnessWeightsSchema>;\n\n/**\n * Fixed default weights, exposed as a transparent, overridable data object (not a\n * config system). Partners can propose alternate weight sets without an API change.\n *\n * @experimental The *shape* (`CompellingnessWeights`) is stable, but these specific\n * default values are a reference methodology that may be re-tuned in a minor release.\n * Pin your own `weights` via {@link ScoreOptions} if you need stable numbers.\n */\nexport const DEFAULT_WEIGHTS: CompellingnessWeights = {\n technology: 0.45,\n biologics: 0.35,\n origin: 0.2,\n};\n\nexport const ScoreResultSchema = z\n .object({\n score: z.number().min(0).max(1).describe('Overall compellingness (consensus point estimate), 0–1.'),\n range: z\n .object({\n low: z.number().min(0).max(1).describe('Most-skeptical reading of the strongest-anomaly signal across competing claims.'),\n high: z.number().min(0).max(1).describe('Most-anomalous reading of the strongest-anomaly signal across competing claims.'),\n })\n .describe(\n 'Spread of the score across competing claims — wide range signals disagreement. Bounds are '\n + 'computed per-signal under the same strongest-anomaly (max) reduction as the point estimate, '\n + 'not a global min/max over every claim.',\n ),\n contested: z\n .boolean()\n .describe('True when claims disagree on *direction* (some assert anomaly, some assert none).'),\n components: z\n .object({\n technology: z.number().min(0).max(1),\n biologics: z.number().min(0).max(1),\n origin: z.number().min(0).max(1),\n })\n .describe('Per-signal consensus contributions before weighting.'),\n weights: CompellingnessWeightsSchema,\n scoringVersion: z.string().describe('Methodology version that produced this score.'),\n })\n .meta({ id: 'ScoreResult' })\n .describe('A reference compellingness score for an observation, with a contestedness range.');\n\nexport type ScoreResult = z.infer<typeof ScoreResultSchema>;\n\n/** The minimal claim shape the trust hook sees (it never needs the full body). */\nexport type EvaluatorWeightInput = {\n evaluatedBy?: string | undefined;\n evidenceRefs?: string[] | undefined;\n};\n\nexport interface ScoreOptions {\n weights?: CompellingnessWeights;\n /** Override the prosaic OCS prefixes (defaults to {@link DEFAULT_PROSAIC_OCS_PREFIXES}). */\n prosaicPrefixes?: readonly string[];\n /**\n * Trust applied to each claim's vote in the consensus point estimate (default\n * 1.0 — uniform). This is where a future trust policy (e.g.\n * weighting verified/official evaluators higher) plugs in here without changing\n * the formula. Range/contestedness are NOT affected — they report the honest\n * spread of what evaluators actually asserted.\n *\n * @experimental The hook signature is stable, but the default trust *policy*\n * (uniform 1.0) is deliberately unopinionated and is a separate, ongoing design\n * task; a future built-in policy may change the default.\n */\n evaluatorWeight?: (claim: EvaluatorWeightInput) => number;\n}\n\nconst UNIFORM_TRUST = (): number => 1;\n\nfunction isProsaic(id: string, prefixes: readonly string[]): boolean {\n return prefixes.some((p) => id === p || id.startsWith(`${p}.`));\n}\n\nfunction round(n: number): number {\n return Math.round(n * 1e4) / 1e4;\n}\n\ntype Weighted = { magnitude: number; weight: number };\n\ninterface ClaimStats {\n /** Trust-weighted mean magnitude (the consensus point). */\n point: number;\n /** Min asserted magnitude (most skeptical). */\n low: number;\n /** Max asserted magnitude (most anomalous). */\n high: number;\n /** Claims disagree on direction (some > 0, some === 0). */\n contested: boolean;\n}\n\nconst EMPTY_STATS: ClaimStats = { point: 0, low: 0, high: 0, contested: false };\n\nfunction reduceClaims(weighted: Weighted[]): ClaimStats {\n if (weighted.length === 0) return EMPTY_STATS;\n let wsum = 0;\n let msum = 0;\n let low = Infinity;\n let high = -Infinity;\n let anyAnomaly = false;\n let anyNone = false;\n for (const { magnitude, weight } of weighted) {\n wsum += weight;\n msum += magnitude * weight;\n if (magnitude < low) low = magnitude;\n if (magnitude > high) high = magnitude;\n if (magnitude > 0) anyAnomaly = true;\n else anyNone = true;\n }\n return {\n point: wsum > 0 ? msum / wsum : 0,\n low,\n high,\n contested: anyAnomaly && anyNone,\n };\n}\n\n/** Asserted anomaly magnitude of one observable claim, in [0,1]. */\nfunction observableMagnitude(claim: ObservableClaim): number {\n return ASSESSMENT_LEVEL_WEIGHTS[claim.level] * (claim.confidence ?? 1);\n}\n\n/**\n * Strongest established anomaly within a domain — the \"is there *any* anomaly?\" reduction.\n * `point`/`low`/`high` are each a **max across observables** of that observable's claim-list\n * stat. So `low` is \"the most-skeptical reading of the strongest-anomaly observable,\" not a\n * global floor across all observables — consistent with the point estimate's max reduction.\n */\nfunction domainStats(\n group: Partial<Record<string, ObservableClaim[]>> | undefined,\n evaluatorWeight: (claim: EvaluatorWeightInput) => number,\n): ClaimStats {\n if (!group) return EMPTY_STATS;\n let point = 0;\n let low = 0;\n let high = 0;\n let contested = false;\n for (const claims of Object.values(group)) {\n if (!claims || claims.length === 0) continue;\n const weighted = claims.map<Weighted>((c) => ({\n magnitude: observableMagnitude(c),\n weight: (c.confidence ?? 1) * evaluatorWeight(c),\n }));\n const stats = reduceClaims(weighted);\n if (stats.point > point) point = stats.point;\n if (stats.low > low) low = stats.low;\n if (stats.high > high) high = stats.high;\n if (stats.contested) contested = true;\n }\n return { point, low, high, contested };\n}\n\n/** Confidence in a *non-prosaic* origin, reduced across competing classifications. */\nfunction originStats(\n origin: OriginClaim[] | undefined,\n prefixes: readonly string[],\n evaluatorWeight: (claim: EvaluatorWeightInput) => number,\n): ClaimStats {\n if (!origin || origin.length === 0) return EMPTY_STATS;\n const weighted = origin.map<Weighted>((c) => ({\n magnitude: isProsaic(c.primaryHypothesis, prefixes) ? 0 : c.confidence,\n weight: c.confidence * evaluatorWeight(c),\n }));\n return reduceClaims(weighted);\n}\n\n/**\n * Reference compellingness scorer. Combines the strongest technology- and\n * biologics-anomaly signals (from `observableAssessments`) with the confidence in\n * a *non-prosaic* origin classification (from `origin`), under transparent,\n * overridable weights. Each slot is a *list* of competing claims, so the scorer\n * reports a consensus `score`, the `range` of readings across claims, and whether\n * the case is `contested` (evaluators disagree on direction). This answers \"how\n * strongly does this case bear on the two public questions — anomalous\n * tech/biologics, and a non-mundane origin?\" — distinct from completeness.\n */\nexport function score(observation: Observation, options?: ScoreOptions): ScoreResult {\n const weights = options?.weights ?? DEFAULT_WEIGHTS;\n const prosaicPrefixes = options?.prosaicPrefixes ?? DEFAULT_PROSAIC_OCS_PREFIXES;\n const evaluatorWeight = options?.evaluatorWeight ?? UNIFORM_TRUST;\n\n const assessments = observation.observableAssessments;\n const technology = domainStats(assessments?.technology, evaluatorWeight);\n const biologics = domainStats(assessments?.biologics, evaluatorWeight);\n const origin = originStats(observation.origin, prosaicPrefixes, evaluatorWeight);\n\n const totalWeight = weights.technology + weights.biologics + weights.origin;\n const combine = (t: number, b: number, o: number): number =>\n totalWeight > 0\n ? (t * weights.technology + b * weights.biologics + o * weights.origin) / totalWeight\n : 0;\n\n return {\n score: round(combine(technology.point, biologics.point, origin.point)),\n range: {\n low: round(combine(technology.low, biologics.low, origin.low)),\n high: round(combine(technology.high, biologics.high, origin.high)),\n },\n contested: technology.contested || biologics.contested || origin.contested,\n components: {\n technology: round(technology.point),\n biologics: round(biologics.point),\n origin: round(origin.point),\n },\n weights,\n scoringVersion: SCORING_VERSION,\n };\n}\n\nexport interface RankedObservation<T extends Observation = Observation> {\n observation: T;\n result: ScoreResult;\n}\n\n/**\n * Score a set of observations and return them sorted most-compelling first — the\n * \"float the strongest cases to the top\" use case.\n */\nexport function rankByCompellingness<T extends Observation>(\n observations: readonly T[],\n options?: ScoreOptions,\n): RankedObservation<T>[] {\n return observations\n .map((observation) => ({ observation, result: score(observation, options) }))\n .sort((a, b) => b.result.score - a.result.score);\n}\n","import { z } from 'zod';\nimport { jsonSchemaEnvelope, schemaId } from '@disclosureos/records/shared';\nimport { CompletenessResultSchema } from './completeness';\nimport { ScoreResultSchema, CompellingnessWeightsSchema } from './compellingness';\n\nexport const SCORING_SCHEMA_VERSION = '2.0.0';\n\nexport const SCORING_SCHEMA_ID = schemaId('scoring', 'scoring', SCORING_SCHEMA_VERSION);\n\n/**\n * Emit the JSON Schema (draft 2020-12) for the scoring output shapes\n * ({@link CompletenessResultSchema}, {@link ScoreResultSchema}) so downstream\n * consumers (and other languages) can validate stored scores.\n */\nexport function scoringJsonSchema(): Record<string, unknown> {\n const bundle = z.toJSONSchema(\n z.object({\n completeness: CompletenessResultSchema,\n compellingness: ScoreResultSchema,\n weights: CompellingnessWeightsSchema,\n }),\n { target: 'draft-2020-12' },\n ) as Record<string, unknown>;\n return jsonSchemaEnvelope(bundle, { id: SCORING_SCHEMA_ID, version: SCORING_SCHEMA_VERSION });\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@disclosureos/scoring",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Reference scoring for the DisclosureOS ecosystem — record completeness and case compellingness across the Observables and Origin layers",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/disclosurefoundation/disclosureos"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/disclosurefoundation/disclosureos#readme",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/disclosurefoundation/disclosureos/issues"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"disclosureos",
|
|
16
|
+
"UAP",
|
|
17
|
+
"scoring",
|
|
18
|
+
"completeness",
|
|
19
|
+
"compellingness",
|
|
20
|
+
"typescript"
|
|
21
|
+
],
|
|
22
|
+
"sideEffects": false,
|
|
23
|
+
"type": "module",
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=20.0.0"
|
|
26
|
+
},
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"import": "./dist/index.js"
|
|
31
|
+
},
|
|
32
|
+
"./completeness": {
|
|
33
|
+
"types": "./dist/completeness/index.d.ts",
|
|
34
|
+
"import": "./dist/completeness/index.js"
|
|
35
|
+
},
|
|
36
|
+
"./compellingness": {
|
|
37
|
+
"types": "./dist/compellingness/index.d.ts",
|
|
38
|
+
"import": "./dist/compellingness/index.js"
|
|
39
|
+
},
|
|
40
|
+
"./schema": "./schema/scoring.schema.json",
|
|
41
|
+
"./package.json": "./package.json"
|
|
42
|
+
},
|
|
43
|
+
"main": "./dist/index.js",
|
|
44
|
+
"types": "./dist/index.d.ts",
|
|
45
|
+
"files": [
|
|
46
|
+
"dist",
|
|
47
|
+
"schema",
|
|
48
|
+
"README.md",
|
|
49
|
+
"CHANGELOG.md",
|
|
50
|
+
"LICENSE"
|
|
51
|
+
],
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"zod": "^4.4.3",
|
|
54
|
+
"@disclosureos/records": "1.0.0",
|
|
55
|
+
"@disclosureos/origins": "1.0.0",
|
|
56
|
+
"@disclosureos/observables": "1.0.0"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@types/node": "^20",
|
|
60
|
+
"tsup": "^8.0.0",
|
|
61
|
+
"tsx": "^4.21.0",
|
|
62
|
+
"typescript": "^5.7.2",
|
|
63
|
+
"vitest": "^4.0.18",
|
|
64
|
+
"@arethetypeswrong/cli": "^0.18.3",
|
|
65
|
+
"publint": "^0.3.21",
|
|
66
|
+
"@repo/typescript-config": "0.0.0"
|
|
67
|
+
},
|
|
68
|
+
"publishConfig": {
|
|
69
|
+
"access": "public"
|
|
70
|
+
},
|
|
71
|
+
"scripts": {
|
|
72
|
+
"build": "tsup",
|
|
73
|
+
"dev": "tsup --watch",
|
|
74
|
+
"lint": "eslint .",
|
|
75
|
+
"type-check": "tsc --noEmit",
|
|
76
|
+
"emit:schema": "tsx scripts/emit-schema.ts",
|
|
77
|
+
"clean": "rm -rf dist",
|
|
78
|
+
"test": "vitest",
|
|
79
|
+
"lint:publish": "publint --strict && attw --pack . --profile esm-only"
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://os.disclosure.org/schema/scoring/2.0.0/scoring.json",
|
|
4
|
+
"x-schema-version": "2.0.0",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"properties": {
|
|
7
|
+
"completeness": {
|
|
8
|
+
"description": "How fully an observation populates the records schema.",
|
|
9
|
+
"$ref": "#/$defs/CompletenessResult"
|
|
10
|
+
},
|
|
11
|
+
"compellingness": {
|
|
12
|
+
"description": "A reference compellingness score for an observation, with a contestedness range.",
|
|
13
|
+
"$ref": "#/$defs/ScoreResult"
|
|
14
|
+
},
|
|
15
|
+
"weights": {
|
|
16
|
+
"description": "Relative weights for the compellingness components. Need not sum to 1 — normalized internally.",
|
|
17
|
+
"$ref": "#/$defs/CompellingnessWeights"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"required": [
|
|
21
|
+
"completeness",
|
|
22
|
+
"compellingness",
|
|
23
|
+
"weights"
|
|
24
|
+
],
|
|
25
|
+
"additionalProperties": false,
|
|
26
|
+
"$defs": {
|
|
27
|
+
"CompletenessResult": {
|
|
28
|
+
"type": "object",
|
|
29
|
+
"properties": {
|
|
30
|
+
"total": {
|
|
31
|
+
"type": "integer",
|
|
32
|
+
"minimum": -9007199254740991,
|
|
33
|
+
"maximum": 9007199254740991,
|
|
34
|
+
"description": "Total leaf field paths defined by the records schema."
|
|
35
|
+
},
|
|
36
|
+
"present": {
|
|
37
|
+
"type": "integer",
|
|
38
|
+
"minimum": -9007199254740991,
|
|
39
|
+
"maximum": 9007199254740991,
|
|
40
|
+
"description": "How many of those paths carry a value."
|
|
41
|
+
},
|
|
42
|
+
"percentage": {
|
|
43
|
+
"type": "integer",
|
|
44
|
+
"minimum": -9007199254740991,
|
|
45
|
+
"maximum": 9007199254740991,
|
|
46
|
+
"description": "present / total, as a 0–100 integer."
|
|
47
|
+
},
|
|
48
|
+
"missing": {
|
|
49
|
+
"type": "array",
|
|
50
|
+
"items": {
|
|
51
|
+
"type": "string"
|
|
52
|
+
},
|
|
53
|
+
"description": "Paths that are absent or empty."
|
|
54
|
+
},
|
|
55
|
+
"requiredTotal": {
|
|
56
|
+
"type": "integer",
|
|
57
|
+
"minimum": -9007199254740991,
|
|
58
|
+
"maximum": 9007199254740991,
|
|
59
|
+
"description": "Count of schema-required leaf paths."
|
|
60
|
+
},
|
|
61
|
+
"requiredPresent": {
|
|
62
|
+
"type": "integer",
|
|
63
|
+
"minimum": -9007199254740991,
|
|
64
|
+
"maximum": 9007199254740991,
|
|
65
|
+
"description": "How many required paths carry a value."
|
|
66
|
+
},
|
|
67
|
+
"requiredPercentage": {
|
|
68
|
+
"type": "integer",
|
|
69
|
+
"minimum": -9007199254740991,
|
|
70
|
+
"maximum": 9007199254740991,
|
|
71
|
+
"description": "requiredPresent / requiredTotal, as a 0–100 integer."
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
"required": [
|
|
75
|
+
"total",
|
|
76
|
+
"present",
|
|
77
|
+
"percentage",
|
|
78
|
+
"missing",
|
|
79
|
+
"requiredTotal",
|
|
80
|
+
"requiredPresent",
|
|
81
|
+
"requiredPercentage"
|
|
82
|
+
],
|
|
83
|
+
"additionalProperties": false
|
|
84
|
+
},
|
|
85
|
+
"CompellingnessWeights": {
|
|
86
|
+
"type": "object",
|
|
87
|
+
"properties": {
|
|
88
|
+
"technology": {
|
|
89
|
+
"type": "number",
|
|
90
|
+
"minimum": 0,
|
|
91
|
+
"description": "Weight of the technology-anomaly signal."
|
|
92
|
+
},
|
|
93
|
+
"biologics": {
|
|
94
|
+
"type": "number",
|
|
95
|
+
"minimum": 0,
|
|
96
|
+
"description": "Weight of the biologics-anomaly signal."
|
|
97
|
+
},
|
|
98
|
+
"origin": {
|
|
99
|
+
"type": "number",
|
|
100
|
+
"minimum": 0,
|
|
101
|
+
"description": "Weight of the (non-prosaic) origin-confidence signal."
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
"required": [
|
|
105
|
+
"technology",
|
|
106
|
+
"biologics",
|
|
107
|
+
"origin"
|
|
108
|
+
],
|
|
109
|
+
"additionalProperties": false
|
|
110
|
+
},
|
|
111
|
+
"ScoreResult": {
|
|
112
|
+
"type": "object",
|
|
113
|
+
"properties": {
|
|
114
|
+
"score": {
|
|
115
|
+
"type": "number",
|
|
116
|
+
"minimum": 0,
|
|
117
|
+
"maximum": 1,
|
|
118
|
+
"description": "Overall compellingness (consensus point estimate), 0–1."
|
|
119
|
+
},
|
|
120
|
+
"range": {
|
|
121
|
+
"type": "object",
|
|
122
|
+
"properties": {
|
|
123
|
+
"low": {
|
|
124
|
+
"type": "number",
|
|
125
|
+
"minimum": 0,
|
|
126
|
+
"maximum": 1,
|
|
127
|
+
"description": "Most-skeptical reading of the strongest-anomaly signal across competing claims."
|
|
128
|
+
},
|
|
129
|
+
"high": {
|
|
130
|
+
"type": "number",
|
|
131
|
+
"minimum": 0,
|
|
132
|
+
"maximum": 1,
|
|
133
|
+
"description": "Most-anomalous reading of the strongest-anomaly signal across competing claims."
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
"required": [
|
|
137
|
+
"low",
|
|
138
|
+
"high"
|
|
139
|
+
],
|
|
140
|
+
"additionalProperties": false,
|
|
141
|
+
"description": "Spread of the score across competing claims — wide range signals disagreement. Bounds are computed per-signal under the same strongest-anomaly (max) reduction as the point estimate, not a global min/max over every claim."
|
|
142
|
+
},
|
|
143
|
+
"contested": {
|
|
144
|
+
"type": "boolean",
|
|
145
|
+
"description": "True when claims disagree on *direction* (some assert anomaly, some assert none)."
|
|
146
|
+
},
|
|
147
|
+
"components": {
|
|
148
|
+
"type": "object",
|
|
149
|
+
"properties": {
|
|
150
|
+
"technology": {
|
|
151
|
+
"type": "number",
|
|
152
|
+
"minimum": 0,
|
|
153
|
+
"maximum": 1
|
|
154
|
+
},
|
|
155
|
+
"biologics": {
|
|
156
|
+
"type": "number",
|
|
157
|
+
"minimum": 0,
|
|
158
|
+
"maximum": 1
|
|
159
|
+
},
|
|
160
|
+
"origin": {
|
|
161
|
+
"type": "number",
|
|
162
|
+
"minimum": 0,
|
|
163
|
+
"maximum": 1
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
"required": [
|
|
167
|
+
"technology",
|
|
168
|
+
"biologics",
|
|
169
|
+
"origin"
|
|
170
|
+
],
|
|
171
|
+
"additionalProperties": false,
|
|
172
|
+
"description": "Per-signal consensus contributions before weighting."
|
|
173
|
+
},
|
|
174
|
+
"weights": {
|
|
175
|
+
"description": "Relative weights for the compellingness components. Need not sum to 1 — normalized internally.",
|
|
176
|
+
"$ref": "#/$defs/CompellingnessWeights"
|
|
177
|
+
},
|
|
178
|
+
"scoringVersion": {
|
|
179
|
+
"type": "string",
|
|
180
|
+
"description": "Methodology version that produced this score."
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
"required": [
|
|
184
|
+
"score",
|
|
185
|
+
"range",
|
|
186
|
+
"contested",
|
|
187
|
+
"components",
|
|
188
|
+
"weights",
|
|
189
|
+
"scoringVersion"
|
|
190
|
+
],
|
|
191
|
+
"additionalProperties": false
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|